Compare commits
133 Commits
bradfitz/a
...
crawshaw/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de94fe0f87 | ||
|
|
dec01ef22b | ||
|
|
7e00100a0a | ||
|
|
fdac0387a7 | ||
|
|
36189e2704 | ||
|
|
bbb4631e04 | ||
|
|
f4ae745b0b | ||
|
|
e923639feb | ||
|
|
d7569863b5 | ||
|
|
52e24aa966 | ||
|
|
4f7d60ad42 | ||
|
|
29b028b9c4 | ||
|
|
54e108ff4e | ||
|
|
20e66c5b92 | ||
|
|
c7e5ab8094 | ||
|
|
ca51529b81 | ||
|
|
741d654aa3 | ||
|
|
1632f9fd6b | ||
|
|
88586ec4a4 | ||
|
|
0c673c1344 | ||
|
|
4cd9218351 | ||
|
|
be906dabd4 | ||
|
|
6680976b50 | ||
|
|
88ab0173a7 | ||
|
|
25321cbd01 | ||
|
|
5378776043 | ||
|
|
6075135e0a | ||
|
|
917307a90c | ||
|
|
34ffd4f7c6 | ||
|
|
de3001bc79 | ||
|
|
11bbfbd8bb | ||
|
|
635e4c7435 | ||
|
|
1ec64bc94d | ||
|
|
7e201806b1 | ||
|
|
1f0fa8b814 | ||
|
|
e101d8396d | ||
|
|
cbd6224ca4 | ||
|
|
4a82e36491 | ||
|
|
9b4e50cec0 | ||
|
|
07c3df13c6 | ||
|
|
e7caad61fb | ||
|
|
6b365b0239 | ||
|
|
e1f773ebba | ||
|
|
6d2b8df06d | ||
|
|
e86b39b73f | ||
|
|
1e7a35b225 | ||
|
|
ddfcc4326c | ||
|
|
a046b48593 | ||
|
|
6064b6ff47 | ||
|
|
138055dd70 | ||
|
|
ace57d7627 | ||
|
|
b9c2231fdf | ||
|
|
fb6b0e247c | ||
|
|
98f9e82c62 | ||
|
|
e8d4afedd1 | ||
|
|
a7562be5e1 | ||
|
|
6f7974b7f2 | ||
|
|
6099ecf7f4 | ||
|
|
7529b74018 | ||
|
|
aa6856a9eb | ||
|
|
d76334d2f0 | ||
|
|
6254efb9ef | ||
|
|
70eb05fd47 | ||
|
|
d37058af72 | ||
|
|
2f0cb98e50 | ||
|
|
f7eed25bb9 | ||
|
|
81466eef81 | ||
|
|
45fe06a89f | ||
|
|
e8cd7bb66f | ||
|
|
9a70789853 | ||
|
|
a2aa6cd2ed | ||
|
|
d139fa9c92 | ||
|
|
267531e4f8 | ||
|
|
717c715c96 | ||
|
|
516e8a4838 | ||
|
|
dd10babaed | ||
|
|
c7d4bf2333 | ||
|
|
2889fabaef | ||
|
|
761188e5d2 | ||
|
|
914a486af6 | ||
|
|
60e189f699 | ||
|
|
006a224f50 | ||
|
|
fe7c3e9c17 | ||
|
|
0bc73f8e4f | ||
|
|
c611d8480b | ||
|
|
c7fc4a06da | ||
|
|
de497358b8 | ||
|
|
1e28207a15 | ||
|
|
7a16ac80b7 | ||
|
|
4d943536f1 | ||
|
|
9f5b0d058f | ||
|
|
4dab0c1702 | ||
|
|
35e10c78fc | ||
|
|
692a011b54 | ||
|
|
e970ed0995 | ||
|
|
a7edcd0872 | ||
|
|
a98538f84a | ||
|
|
c3c59445ff | ||
|
|
0dde8fa0a8 | ||
|
|
4d3c09ced4 | ||
|
|
567c5a6d9e | ||
|
|
4fea604979 | ||
|
|
bf6205d200 | ||
|
|
9f7cbf6cf1 | ||
|
|
9ce92aad3e | ||
|
|
fa3543d629 | ||
|
|
e7bf144c3f | ||
|
|
97496a83af | ||
|
|
eb47cba435 | ||
|
|
daf2c70a08 | ||
|
|
d5baeeed5c | ||
|
|
4306433d1c | ||
|
|
9541886856 | ||
|
|
49d00b6a28 | ||
|
|
54d0d83b67 | ||
|
|
fec9490378 | ||
|
|
c55d26967b | ||
|
|
9f1b02699a | ||
|
|
a905ce5607 | ||
|
|
359055d3fa | ||
|
|
b5628cee4e | ||
|
|
edf64e0901 | ||
|
|
ec77b80c53 | ||
|
|
b5b4992eff | ||
|
|
d3dd7c6270 | ||
|
|
187e22a756 | ||
|
|
ab9cccb292 | ||
|
|
78338ac029 | ||
|
|
b405644f5d | ||
|
|
5fe5402fcd | ||
|
|
e4c075cd95 | ||
|
|
edce91a8a6 | ||
|
|
51bd1feae4 |
@@ -48,6 +48,9 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG goflags_arg # default intentionally unset
|
||||
ENV GOFLAGS=$goflags_arg
|
||||
|
||||
RUN go install -v ./cmd/...
|
||||
|
||||
FROM alpine:3.11
|
||||
|
||||
46
LICENSE
46
LICENSE
@@ -1,27 +1,29 @@
|
||||
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Tailscale Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.3.0
|
||||
1.5.0
|
||||
|
||||
62
api.md
62
api.md
@@ -5,7 +5,28 @@ The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JS
|
||||
# Authentication
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
|
||||
|
||||
# APIS
|
||||
# APIs
|
||||
|
||||
* **[Devices](#device)**
|
||||
- [GET device](#device-get)
|
||||
- [DELETE device](#device-delete)
|
||||
- Routes
|
||||
- [GET device routes](#device-routes-get)
|
||||
- [POST device routes](#device-routes-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [DNS](#tailnet-dns)
|
||||
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
|
||||
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
|
||||
- [GET tailnet DNS preferences](#tailnet-dns-preferences-get)
|
||||
- [POST tailnet DNS preferences](#tailnet-dns-preferences-post)
|
||||
- [GET tailnet DNS searchpaths](#tailnet-dns-searchpaths-get)
|
||||
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
|
||||
|
||||
## Device
|
||||
<!-- TODO: description about what devices are -->
|
||||
@@ -16,6 +37,8 @@ To find the deviceID of a particular device, you can use the ["GET /devices"](#g
|
||||
Find the device you're looking for and get the "id" field.
|
||||
This is your deviceID.
|
||||
|
||||
<a name=device-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
Returns the details for the specified device.
|
||||
Supply the device of interest in the path using its ID.
|
||||
@@ -103,6 +126,7 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-delete></div>
|
||||
|
||||
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
|
||||
Deletes the provided device from its tailnet.
|
||||
@@ -139,6 +163,8 @@ If the device is not owned by your tailnet:
|
||||
```
|
||||
|
||||
|
||||
<a name=device-routes-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
|
||||
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
|
||||
@@ -166,6 +192,8 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-routes-post></div>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
|
||||
|
||||
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
|
||||
@@ -210,7 +238,8 @@ A tailnet is the name of your Tailscale network.
|
||||
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
|
||||
|
||||
|
||||
"alice@example.com" belongs to the "example.com" tailnet and would use the following format for API calls:
|
||||
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
|
||||
@@ -218,21 +247,21 @@ curl https://api.tailscale.com/api/v2/tailnet/example.com/...
|
||||
|
||||
|
||||
For solo plans, the tailnet is the email you signed up with.
|
||||
So "alice@gmail.com" has the tailnet "alice@gmail.com" since @gmail.com is a shared email host.
|
||||
So `alice@gmail.com` has the tailnet `alice@gmail.com` since `@gmail.com` is a shared email host.
|
||||
Her API calls would have the following format:
|
||||
```
|
||||
GET /api/v2/tailnet/alice@gmail.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
|
||||
```
|
||||
|
||||
Tailnets are a top level resource. ACL is an example of a resource that is tied to a top level tailnet.
|
||||
|
||||
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
|
||||
|
||||
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
|
||||
|
||||
|
||||
### ACL
|
||||
|
||||
<a name=tailnet-acl-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/acl` - fetch ACL for a tailnet
|
||||
|
||||
Retrieves the ACL that is currently set for the given tailnet. Supply the tailnet of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
|
||||
@@ -334,6 +363,8 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl` - set ACL for a tailnet
|
||||
|
||||
Sets the ACL for the given tailnet. HuJSON and JSON are both accepted inputs. An `If-Match` header can be set to avoid missed updates.
|
||||
@@ -405,6 +436,8 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-preview-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/preview` - preview rule matches on an ACL for a resource
|
||||
Determines what rules match for a user on an ACL without saving the ACL to the server.
|
||||
|
||||
@@ -449,8 +482,12 @@ Response
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
|
||||
<a name=tailnet-devices-get></a>
|
||||
|
||||
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
|
||||
Lists the devices in a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -531,9 +568,12 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns></a>
|
||||
|
||||
### DNS
|
||||
|
||||
<a name=tailnet-dns-nameservers-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
|
||||
Lists the DNS nameservers for a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -556,6 +596,8 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-nameservers-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
|
||||
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -608,6 +650,8 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-preferences-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/preferences` - retrieves the DNS preferences for a tailnet
|
||||
Retrieves the DNS preferences that are currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -629,6 +673,8 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-preferences-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
|
||||
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
|
||||
Note that MagicDNS is dependent on DNS servers.
|
||||
@@ -673,6 +719,8 @@ If there are DNS servers:
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-searchpaths-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
|
||||
Retrieves the list of search paths that is currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -695,6 +743,8 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-searchpaths-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
|
||||
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.
|
||||
|
||||
|
||||
31
build_docker.sh
Executable file
31
build_docker.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Runs `go build` with flags configured for docker distribution. All
|
||||
# it does differently from `go build` is burn git commit and version
|
||||
# information into the binaries inside docker, so that we can track down user
|
||||
# issues.
|
||||
#
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
|
||||
set -eu
|
||||
|
||||
eval $(./version/version.sh)
|
||||
|
||||
GOFLAGS='-tags xversion -ldflags '"-X tailscale.com/version.Long=${VERSION_LONG} -X tailscale.com/version.Short=${VERSION_SHORT} -X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}"
|
||||
docker build --build-arg goflags_arg="'""${GOFLAGS}""'" -t tailscale:tailscale .
|
||||
@@ -5,6 +5,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -40,6 +41,6 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
c.MeshKey = s.MeshKey()
|
||||
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
|
||||
go c.RunWatchConnectionLoop(s.PublicKey(), add, remove)
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
208
cmd/hello/hello.go
Normal file
208
cmd/hello/hello.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The hello binary runs hello.ipn.dev.
|
||||
package main // import "tailscale.com/cmd/hello"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
|
||||
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
|
||||
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *testIP != "" {
|
||||
res, err := whoIs(*testIP)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(res)
|
||||
return
|
||||
}
|
||||
if !devMode() {
|
||||
tmpl = template.Must(template.New("home").Parse(slurpHTML()))
|
||||
}
|
||||
|
||||
http.HandleFunc("/", root)
|
||||
log.Printf("Starting hello server.")
|
||||
|
||||
errc := make(chan error, 1)
|
||||
if *httpAddr != "" {
|
||||
log.Printf("running HTTP server on %s", *httpAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServe(*httpAddr, nil)
|
||||
}()
|
||||
}
|
||||
if *httpsAddr != "" {
|
||||
log.Printf("running HTTPS server on %s", *httpsAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServeTLS(*httpsAddr,
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
nil,
|
||||
)
|
||||
}()
|
||||
}
|
||||
log.Fatal(<-errc)
|
||||
}
|
||||
|
||||
func slurpHTML() string {
|
||||
slurp, err := ioutil.ReadFile("hello.tmpl.html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return string(slurp)
|
||||
}
|
||||
|
||||
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
||||
|
||||
func getTmpl() (*template.Template, error) {
|
||||
if devMode() {
|
||||
return template.New("home").Parse(slurpHTML())
|
||||
}
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
var tmpl *template.Template // not used in dev mode, initialized by main after flag parse
|
||||
|
||||
type tmplData struct {
|
||||
DisplayName string // "Foo Barberson"
|
||||
LoginName string // "foo@bar.com"
|
||||
ProfilePicURL string // "https://..."
|
||||
MachineName string // "imac5k"
|
||||
MachineOS string // "Linux"
|
||||
IP string // "100.2.3.4"
|
||||
}
|
||||
|
||||
func root(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil && *httpsAddr != "" {
|
||||
host := r.Host
|
||||
if strings.Contains(r.Host, "100.101.102.103") {
|
||||
host = "hello.ipn.dev"
|
||||
}
|
||||
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if r.RequestURI != "/" {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
http.Error(w, "no remote addr", 500)
|
||||
return
|
||||
}
|
||||
tmpl, err := getTmpl()
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
http.Error(w, "template error: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
who, err := whoIs(ip)
|
||||
var data tmplData
|
||||
if err != nil {
|
||||
if devMode() {
|
||||
log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
|
||||
data = tmplData{
|
||||
DisplayName: "Taily Scalerson",
|
||||
LoginName: "taily@scaler.son",
|
||||
ProfilePicURL: "https://placekitten.com/200/200",
|
||||
MachineName: "scaled",
|
||||
MachineOS: "Linux",
|
||||
IP: "100.1.2.3",
|
||||
}
|
||||
} else {
|
||||
log.Printf("whois(%q) error: %v", ip, err)
|
||||
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
data = tmplData{
|
||||
DisplayName: who.UserProfile.DisplayName,
|
||||
LoginName: who.UserProfile.LoginName,
|
||||
ProfilePicURL: who.UserProfile.ProfilePicURL,
|
||||
MachineName: firstLabel(who.Node.ComputedName),
|
||||
MachineOS: who.Node.Hostinfo.OS,
|
||||
IP: ip,
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
// firstLabel s up until the first period, if any.
|
||||
func firstLabel(s string) string {
|
||||
if i := strings.Index(s, "."); i != -1 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// tsSockClient does HTTP requests to the local Tailscale daemon.
|
||||
// The hostname in the HTTP request is ignored.
|
||||
var tsSockClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
return safesocket.ConnectDefault()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func whoIs(ip string) (*tailcfg.WhoIsResponse, error) {
|
||||
ctx := context.Background()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
req.SetBasicAuth("", token)
|
||||
}
|
||||
res, err := tsSockClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
slurp, _ := ioutil.ReadAll(res.Body)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
|
||||
}
|
||||
r := new(tailcfg.WhoIsResponse)
|
||||
if err := json.Unmarshal(slurp, r); err != nil {
|
||||
if max := 200; len(slurp) > max {
|
||||
slurp = slurp[:max]
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
436
cmd/hello/hello.tmpl.html
Normal file
436
cmd/hello/hello.tmpl.html
Normal file
@@ -0,0 +1,436 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
<title>Hello from Tailscale</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: #dad6d5;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 24rem;
|
||||
width: 95%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-3 {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.pr-3 {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mb-12 {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.width-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.min-width-0 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-t-1 {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-gray-100 {
|
||||
border-color: #f7f5f4;
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
border-color: #eeebea;
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
border-color: #dad6d5;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.bg-gray-0 {
|
||||
background-color: #faf9f8;
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
background-color: #f7f5f4;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #0d4b3b;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #3f5db3;
|
||||
}
|
||||
|
||||
.hover\:text-blue-800:hover {
|
||||
color: #253570;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #444342;
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
color: #2e2d2d;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #232222;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.font-title {
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 9999px;
|
||||
background-size: cover;
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.animate .panel {
|
||||
transform: translateY(10%);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.0), 0 10px 10px -5px rgba(0, 0, 0, 0.0);
|
||||
transition: transform 1200ms ease, opacity 1200ms ease, box-shadow 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .panel-interior {
|
||||
opacity: 0.0;
|
||||
transition: opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .logo {
|
||||
transform: translateY(2rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .header-title {
|
||||
transform: translateY(1.6rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .header-text {
|
||||
transform: translateY(1.2rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .footer {
|
||||
transform: translateY(-0.5rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animating .panel {
|
||||
transform: translateY(0);
|
||||
opacity: 1.0;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.animating .panel-interior {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.animating .spinner {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.animating .logo,
|
||||
.animating .header-title,
|
||||
.animating .header-text,
|
||||
.animating .footer {
|
||||
transform: translateY(0);
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
align-items: center;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
.spinner span {
|
||||
display: inline-block;
|
||||
background-color: currentColor;
|
||||
border-radius: 9999px;
|
||||
animation-name: loading-dots-blink;
|
||||
animation-duration: 1.4s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-fill-mode: both;
|
||||
width: 0.35em;
|
||||
height: 0.35em;
|
||||
margin: 0 0.15em;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(2) {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(3) {
|
||||
animation-delay: 400ms;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.animate .spinner {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@keyframes loading-dots-blink {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
* {
|
||||
animation-duration: 0ms !important;
|
||||
transition-duration: 0ms !important;
|
||||
transition-delay: 0ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<script>
|
||||
(function() {
|
||||
var lastSeen = localStorage.getItem("lastSeen");
|
||||
if (!lastSeen) {
|
||||
document.body.classList.add("animate");
|
||||
window.addEventListener("load", function () {
|
||||
setTimeout(function () {
|
||||
document.body.classList.add("animating");
|
||||
localStorage.setItem("lastSeen", Date.now());
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<main class="text-gray-800">
|
||||
<svg class="logo mb-6" width="28" height="28" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor" />
|
||||
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor" />
|
||||
</svg>
|
||||
<header class="mb-8 text-center">
|
||||
<h1 class="header-title font-title font-semibold mb-2">You're connected over Tailscale!</h1>
|
||||
<p class="header-text">This device is signed in as…</p>
|
||||
</header>
|
||||
<div class="panel relative bg-white rounded-lg width-full shadow-xl mb-8 p-4">
|
||||
<div class="spinner text-gray-600">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="panel-interior flex items-center width-full min-width-0 p-2 mb-4">
|
||||
<div class="profile-pic bg-gray-100" style="background-image: url({{.ProfilePicURL}});"></div>
|
||||
<div class="overflow-hidden">
|
||||
{{ with .DisplayName }}
|
||||
<h4 class="font-semibold truncate">{{.}}</h4>
|
||||
{{ end }}
|
||||
<h5 class="text-gray-600 truncate">{{.LoginName}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="panel-interior border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-2 width-full flex justify-between items-center">
|
||||
<div class="flex items-center min-width-0">
|
||||
<svg class="text-gray-600 mr-2" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>
|
||||
<h4 class="font-semibold truncate mr-2">{{.MachineName}}</h4>
|
||||
</div>
|
||||
<h5>{{.IP}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer text-gray-600 text-center mb-12">
|
||||
<p>Read about <a href="https://tailscale.com/kb/1017/install#advanced-features" class="text-blue-600 hover:text-blue-800"
|
||||
target="_blank">what you can do next →</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -68,11 +68,6 @@ change in the future.
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
|
||||
// Don't advertise the debug command, but it exists.
|
||||
if strSliceContains(args, "debug") {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -134,12 +129,3 @@ func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func strSliceContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -64,34 +64,63 @@ func runPing(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 {
|
||||
if len(args) != 1 || args[0] == "" {
|
||||
return errors.New("usage: ping <hostname-or-IP>")
|
||||
}
|
||||
hostOrIP := args[0]
|
||||
var ip string
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
ch := make(chan *ipnstate.PingResult, 1)
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
stc := make(chan *ipnstate.Status, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
ch <- pr
|
||||
prc <- pr
|
||||
}
|
||||
if n.Status != nil {
|
||||
stc <- n.Status
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
hostOrIP := args[0]
|
||||
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
ip = hostOrIP
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
if ip == "" {
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case st := <-stc:
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
ip = ps.TailAddr
|
||||
break
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
if ip == "" {
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
n := 0
|
||||
anyPong := false
|
||||
for {
|
||||
@@ -101,7 +130,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case pr := <-ch:
|
||||
case pr := <-prc:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
return errors.New(pr.Err)
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -66,7 +65,17 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
ch <- n.Status
|
||||
select {
|
||||
case ch <- n.Status:
|
||||
default:
|
||||
// A status update from somebody else's request.
|
||||
// Ignoring this matters mostly for "tailscale status -web"
|
||||
// mode, otherwise the channel send would block forever
|
||||
// and pump would stop reading from tailscaled, which
|
||||
// previously caused tailscaled to block (while holding
|
||||
// a mutex), backing up unrelated clients.
|
||||
// See https://github.com/tailscale/tailscale/issues/1234
|
||||
}
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
@@ -150,13 +159,18 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
if !active {
|
||||
if anyTraffic {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node")
|
||||
} else if anyTraffic {
|
||||
f("idle")
|
||||
} else {
|
||||
f("-")
|
||||
}
|
||||
} else {
|
||||
f("active; ")
|
||||
if ps.ExitNode {
|
||||
f("exit node; ")
|
||||
}
|
||||
if relay != "" && ps.CurAddr == "" {
|
||||
f("relay %q", relay)
|
||||
} else if ps.CurAddr != "" {
|
||||
@@ -181,7 +195,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
|
||||
ipnstate.SortPeers(peers)
|
||||
for _, ps := range peers {
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
@@ -211,16 +225,6 @@ func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_"))
|
||||
}
|
||||
|
||||
func sortKey(ps *ipnstate.PeerStatus) string {
|
||||
if ps.DNSName != "" {
|
||||
return ps.DNSName
|
||||
}
|
||||
if ps.HostName != "" {
|
||||
return ps.HostName
|
||||
}
|
||||
return ps.TailAddr
|
||||
}
|
||||
|
||||
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
if ps.UserID.IsZero() {
|
||||
return "-"
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
var upCmd = &ffcli.Command{
|
||||
@@ -45,6 +45,7 @@ specify any flags, options are reset to their default.
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
|
||||
@@ -74,6 +75,7 @@ var upArgs struct {
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
exitNodeIP string
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
@@ -120,6 +122,11 @@ func checkIPForwarding() {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
@@ -133,12 +140,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.exitNodeIP != "" {
|
||||
return errors.New("--exit-node is " + notSupported)
|
||||
}
|
||||
if upArgs.netfilterMode != "off" {
|
||||
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
@@ -149,11 +160,30 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routes = append(routes, ipp)
|
||||
}
|
||||
if default4 && !default6 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
if err != nil {
|
||||
fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
@@ -174,6 +204,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
prefs.ExitNodeIP = exitNodeIP
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
@@ -186,12 +217,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = router.NetfilterOn
|
||||
prefs.NetfilterMode = preftype.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = router.NetfilterNoDivert
|
||||
prefs.NetfilterMode = preftype.NetfilterNoDivert
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = router.NetfilterOff
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
@@ -212,7 +243,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
fatalf("backend error: %v\n", *n.ErrMessage)
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
msg += " (try 'sudo tailscale up [...]')"
|
||||
}
|
||||
}
|
||||
fatalf("backend error: %v\n", msg)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
|
||||
@@ -4,122 +4,83 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/atomicfile from tailscale.com/ipn
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn+
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine from tailscale.com/ipn
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/derp
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from tailscale.com/ipn+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
@@ -128,7 +89,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http+
|
||||
compress/gzip from net/http
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
@@ -156,7 +117,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
encoding from encoding/json+
|
||||
encoding from encoding/json
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
@@ -170,7 +131,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate
|
||||
io from bufio+
|
||||
@@ -179,7 +139,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
math/rand from math/big+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
@@ -190,23 +150,21 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
L os/user from github.com/godbus/dbus/v5
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp from rsc.io/goversion/version
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/interfaces"
|
||||
@@ -28,28 +27,26 @@ import (
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
var debugModeFunc = debugMode // so it can be addressable
|
||||
|
||||
func debugMode(args []string) error {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fs.Args()) > 0 {
|
||||
return errors.New("unknown non-flag debug subcommand arguments")
|
||||
}
|
||||
ctx := context.Background()
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
@@ -2,8 +2,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
@@ -22,7 +20,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
@@ -31,7 +28,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
@@ -69,15 +65,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
@@ -87,32 +86,36 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/wgengine/tsdns+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
@@ -123,12 +126,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
@@ -152,15 +158,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
|
||||
142
cmd/tailscaled/install_darwin.go
Normal file
142
cmd/tailscaled/install_darwin.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func init() {
|
||||
installSystemDaemon = installSystemDaemonDarwin
|
||||
uninstallSystemDaemon = uninstallSystemDaemonDarwin
|
||||
}
|
||||
|
||||
// darwinLaunchdPlist is the launchd.plist that's written to
|
||||
// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
|
||||
// future) a user-specific location.
|
||||
//
|
||||
// See man launchd.plist.
|
||||
const darwinLaunchdPlist = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
|
||||
<key>Label</key>
|
||||
<string>com.tailscale.tailscaled</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/tailscaled</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
|
||||
const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
|
||||
const targetBin = "/usr/local/bin/tailscaled"
|
||||
const service = "com.tailscale.tailscaled"
|
||||
|
||||
func uninstallSystemDaemonDarwin(args []string) (ret error) {
|
||||
if len(args) > 0 {
|
||||
return errors.New("uninstall subcommand takes no arguments")
|
||||
}
|
||||
|
||||
plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
|
||||
_ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
|
||||
running := err == nil
|
||||
|
||||
if running {
|
||||
out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
|
||||
ret = err
|
||||
}
|
||||
out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
|
||||
if ret == nil {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Remove(sysPlist)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
if ret == nil {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func installSystemDaemonDarwin(args []string) (err error) {
|
||||
if len(args) > 0 {
|
||||
return errors.New("install subcommand takes no arguments")
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && os.Getuid() != 0 {
|
||||
err = fmt.Errorf("%w; try running tailscaled with sudo", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Copy ourselves to /usr/local/bin/tailscaled.
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find our own executable path: %w", err)
|
||||
}
|
||||
tmpBin := targetBin + ".tmp"
|
||||
f, err := os.Create(tmpBin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self, err := os.Open(exe)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, self)
|
||||
self.Close()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(tmpBin, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpBin, targetBin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Best effort:
|
||||
uninstallSystemDaemonDarwin(nil)
|
||||
|
||||
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
|
||||
}
|
||||
|
||||
if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/paths"
|
||||
@@ -53,6 +52,10 @@ func defaultTunName() string {
|
||||
return "tun"
|
||||
case "windows":
|
||||
return "Tailscale"
|
||||
case "darwin":
|
||||
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
|
||||
// as a magic value that uses/creates any free number.
|
||||
return "utun"
|
||||
}
|
||||
return "tailscale0"
|
||||
}
|
||||
@@ -68,6 +71,17 @@ var args struct {
|
||||
verbose int
|
||||
}
|
||||
|
||||
var (
|
||||
installSystemDaemon func([]string) error // non-nil on some platforms
|
||||
uninstallSystemDaemon func([]string) error // non-nil on some platforms
|
||||
)
|
||||
|
||||
var subCommands = map[string]*func([]string) error{
|
||||
"install-system-daemon": &installSystemDaemon,
|
||||
"uninstall-system-daemon": &uninstallSystemDaemon,
|
||||
"debug": &debugModeFunc,
|
||||
}
|
||||
|
||||
func main() {
|
||||
// We aren't very performance sensitive, and the parts that are
|
||||
// performance sensitive (wireguard) try hard not to do any memory
|
||||
@@ -88,9 +102,23 @@ func main() {
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Fatalf("fixConsoleOutput: %v", err)
|
||||
if len(os.Args) > 1 {
|
||||
sub := os.Args[1]
|
||||
if fp, ok := subCommands[sub]; ok {
|
||||
if *fp == nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("%s not available on %v", sub, runtime.GOOS)
|
||||
}
|
||||
if err := (*fp)(os.Args[2:]); err != nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if beWindowsSubprocess() {
|
||||
return
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
@@ -103,10 +131,6 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
if args.socketpath == "" && runtime.GOOS != "windows" {
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
@@ -129,6 +153,16 @@ func run() error {
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
if isWindowsService() {
|
||||
// Run the IPN server from the Windows service manager.
|
||||
log.Printf("Running service...")
|
||||
if err := runWindowsService(pol); err != nil {
|
||||
log.Printf("runservice: %v", err)
|
||||
}
|
||||
log.Printf("Service ended.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var logf logger.Logf = log.Printf
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
@@ -140,6 +174,10 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
|
||||
@@ -20,22 +20,5 @@ CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
Type=notify
|
||||
|
||||
DeviceAllow=/dev/net/tun
|
||||
DeviceAllow=/dev/null
|
||||
DeviceAllow=/dev/random
|
||||
DeviceAllow=/dev/urandom
|
||||
DevicePolicy=strict
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
PrivateTmp=true
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/etc/
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
15
cmd/tailscaled/tailscaled_notwindows.go
Normal file
15
cmd/tailscaled/tailscaled_notwindows.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import "tailscale.com/logpolicy"
|
||||
|
||||
func isWindowsService() bool { return false }
|
||||
|
||||
func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
|
||||
|
||||
func beWindowsSubprocess() bool { return false }
|
||||
180
cmd/tailscaled/tailscaled_windows.go
Normal file
180
cmd/tailscaled/tailscaled_windows.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
// TODO: check if administrator, like tswin does.
|
||||
//
|
||||
// TODO: try to load wintun.dll early at startup, before wireguard/tun
|
||||
// does (which panics) and if we'd fail (e.g. due to access
|
||||
// denied, even if administrator), use 'tasklist /m wintun.dll'
|
||||
// to see if something else is currently using it and tell user.
|
||||
//
|
||||
// TODO: check if Tailscale service is already running, and fail early
|
||||
// like tswin does.
|
||||
//
|
||||
// TODO: on failure, check if on a UNC drive and recommend copying it
|
||||
// to C:\ to run it, like tswin does.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
const serviceName = "Tailscale"
|
||||
|
||||
func isWindowsService() bool {
|
||||
v, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
log.Fatalf("svc.IsWindowsService failed: %v", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func runWindowsService(pol *logpolicy.Policy) error {
|
||||
return svc.Run(serviceName, &ipnService{Policy: pol})
|
||||
}
|
||||
|
||||
type ipnService struct {
|
||||
Policy *logpolicy.Policy
|
||||
}
|
||||
|
||||
// Called by Windows to execute the windows service.
|
||||
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
args := []string{"/subproc", service.Policy.PublicID.String()}
|
||||
ipnserver.BabysitProc(ctx, args, log.Printf)
|
||||
}()
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
select {
|
||||
case <-doneCh:
|
||||
case cmd := <-r:
|
||||
switch cmd.Cmd {
|
||||
case svc.Stop:
|
||||
cancel()
|
||||
case svc.Interrogate:
|
||||
changes <- cmd.CurrentStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return false, windows.NO_ERROR
|
||||
}
|
||||
|
||||
func beWindowsSubprocess() bool {
|
||||
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
|
||||
return false
|
||||
}
|
||||
logid := os.Args[2]
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
for {
|
||||
_, err := os.Stdin.Read(b)
|
||||
if err != nil {
|
||||
log.Fatalf("stdin err (parent process died): %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := startIPNServer(context.Background(), logid)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver: %v", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
var eng wgengine.Engine
|
||||
var err error
|
||||
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, "Tailscale", 41641)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wgengine.NewWatchdog(eng), nil
|
||||
}
|
||||
|
||||
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
err = fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
} else {
|
||||
// We have a bunch of bug reports of wgengine.NewUserspaceEngine returning a few different errors,
|
||||
// all intermittently. A few times I (Brad) have also seen sporadic failures that simply
|
||||
// restarting fixed. So try a few times.
|
||||
for try := 1; try <= 5; try++ {
|
||||
if try > 1 {
|
||||
// Only sleep a bit. Don't do some massive backoff because
|
||||
// the frontend GUI has a 30 second timeout on connecting to us,
|
||||
// but even 5 seconds is too long for them to get any results.
|
||||
// 5 tries * 1 second each seems fine.
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
eng, err = getEngine()
|
||||
if err != nil {
|
||||
logf("wgengine.NewUserspaceEngine: (try %v) %v", try, err)
|
||||
continue
|
||||
}
|
||||
if try > 1 {
|
||||
logf("wgengine.NewUserspaceEngine: ended up working on try %v", try)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// Log the error, but don't fatalf. We want to
|
||||
// propagate the error message to the UI frontend. So
|
||||
// we continue and tell the ipnserver to return that
|
||||
// in a Notify message.
|
||||
logf("wgengine.NewUserspaceEngine: %v", err)
|
||||
}
|
||||
opts := ipnserver.Options{
|
||||
Port: 41112,
|
||||
SurviveDisconnects: false,
|
||||
StatePath: args.statepath,
|
||||
}
|
||||
if err != nil {
|
||||
// Return nicer errors to users, annotated with logids, which helps
|
||||
// when they file bugs.
|
||||
rawGetEngine := getEngine // raw == without verbose logid-containing error
|
||||
getEngine = func() (wgengine.Engine, error) {
|
||||
eng, err := rawGetEngine()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wgengine.NewUserspaceEngine: %v\n\nlogid: %v", err, logid)
|
||||
}
|
||||
return eng, nil
|
||||
}
|
||||
} else {
|
||||
getEngine = ipnserver.FixedEngine(eng)
|
||||
}
|
||||
err = ipnserver.Run(ctx, logf, logid, getEngine, opts)
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -32,7 +32,9 @@ import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -96,7 +98,13 @@ func handleSSH(s ssh.Session) {
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !interfaces.IsTailscaleIP(ta.IP) {
|
||||
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
|
||||
if !ok {
|
||||
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(tanetaddr) {
|
||||
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
@@ -68,9 +70,9 @@ type Status struct {
|
||||
LoginFinished *empty.Message
|
||||
Err string
|
||||
URL string
|
||||
Persist *Persist // locally persisted configuration
|
||||
NetMap *NetworkMap // server-pushed configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
Persist *persist.Persist // locally persisted configuration
|
||||
NetMap *netmap.NetworkMap // server-pushed configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
State State
|
||||
}
|
||||
|
||||
@@ -213,7 +215,7 @@ func (c *Client) sendNewMapRequest() {
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
// in a lite update, then tear down everything and start a new stream
|
||||
// (which starts by sending a new map request)
|
||||
if !c.inPollNetMap || c.inLiteMapUpdate {
|
||||
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
|
||||
c.mu.Unlock()
|
||||
c.cancelMapSafely()
|
||||
return
|
||||
@@ -509,7 +511,7 @@ func (c *Client) mapRoutine() {
|
||||
c.inPollNetMap = false
|
||||
c.mu.Unlock()
|
||||
|
||||
err := c.direct.PollNetMap(ctx, -1, func(nm *NetworkMap) {
|
||||
err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) {
|
||||
c.mu.Lock()
|
||||
|
||||
select {
|
||||
@@ -606,7 +608,7 @@ func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
|
||||
func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
func (c *Client) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
|
||||
c.mu.Lock()
|
||||
state := c.state
|
||||
loggedIn := c.loggedIn
|
||||
@@ -618,7 +620,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
|
||||
var p *Persist
|
||||
var p *persist.Persist
|
||||
var fin *empty.Message
|
||||
if state == StateAuthenticated {
|
||||
fin = new(empty.Message)
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
package controlclient
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=direct_clone.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
@@ -41,70 +39,15 @@ import (
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
type Persist struct {
|
||||
_ structs.Incomparable
|
||||
|
||||
// LegacyFrontendPrivateMachineKey is here temporarily
|
||||
// (starting 2020-09-28) during migration of Windows users'
|
||||
// machine keys from frontend storage to the backend. On the
|
||||
// first LocalBackend.Start call, the backend will initialize
|
||||
// the real (backend-owned) machine key from the frontend's
|
||||
// provided value (if non-zero), picking a new random one if
|
||||
// needed. This field should be considered read-only from GUI
|
||||
// frontends. The real value should not be written back in
|
||||
// this field, lest the frontend persist it to disk.
|
||||
LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"`
|
||||
|
||||
PrivateNodeKey wgkey.Private
|
||||
OldPrivateNodeKey wgkey.Private // needed to request key rotation
|
||||
Provider string
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func (p *Persist) Equals(p2 *Persist) bool {
|
||||
if p == nil && p2 == nil {
|
||||
return true
|
||||
}
|
||||
if p == nil || p2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
|
||||
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
|
||||
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
||||
p.Provider == p2.Provider &&
|
||||
p.LoginName == p2.LoginName
|
||||
}
|
||||
|
||||
func (p *Persist) Pretty() string {
|
||||
var mk, ok, nk wgkey.Key
|
||||
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
mk = p.LegacyFrontendPrivateMachineKey.Public()
|
||||
}
|
||||
if !p.OldPrivateNodeKey.IsZero() {
|
||||
ok = p.OldPrivateNodeKey.Public()
|
||||
}
|
||||
if !p.PrivateNodeKey.IsZero() {
|
||||
nk = p.PrivateNodeKey.Public()
|
||||
}
|
||||
ss := func(k wgkey.Key) string {
|
||||
if k.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return k.ShortString()
|
||||
}
|
||||
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
|
||||
ss(mk), ss(ok), ss(nk), p.LoginName)
|
||||
}
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
@@ -121,7 +64,7 @@ type Direct struct {
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgkey.Key
|
||||
persist Persist
|
||||
persist persist.Persist
|
||||
authKey string
|
||||
tryingNewKey wgkey.Private
|
||||
expiry *time.Time
|
||||
@@ -133,7 +76,7 @@ type Direct struct {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Persist Persist // initial persistent data
|
||||
Persist persist.Persist // initial persistent data
|
||||
MachinePrivateKey wgkey.Private // the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
@@ -271,7 +214,7 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Direct) GetPersist() Persist {
|
||||
func (c *Direct) GetPersist() persist.Persist {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.persist
|
||||
@@ -294,7 +237,7 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
// immediately invalidated.
|
||||
//if !c.persist.PrivateNodeKey.IsZero() {
|
||||
//}
|
||||
c.persist = Persist{}
|
||||
c.persist = persist.Persist{}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -526,7 +469,7 @@ func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
//
|
||||
// maxPolls is how many network maps to download; common values are 1
|
||||
// or -1 (to keep a long-poll query open to the server).
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
return c.sendMapRequest(ctx, maxPolls, cb)
|
||||
}
|
||||
|
||||
@@ -538,7 +481,7 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// cb nil means to omit peers.
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
@@ -550,6 +493,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
everEndpoints := c.everEndpoints
|
||||
c.mu.Unlock()
|
||||
|
||||
if persist.PrivateNodeKey.IsZero() {
|
||||
return errors.New("privateNodeKey is zero")
|
||||
}
|
||||
if backendLogID == "" {
|
||||
return errors.New("hostinfo: BackendLogID missing")
|
||||
}
|
||||
@@ -744,6 +690,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
}
|
||||
resp.Peers = filtered
|
||||
}
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
// We need at least one endpoint here for now else
|
||||
// other code doesn't even create the discoEndpoint.
|
||||
// TODO(bradfitz): fix that and then just nil this out.
|
||||
p.Endpoints = []string{"127.9.9.9:456"}
|
||||
}
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||
@@ -761,7 +715,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
localPort = c.localPort
|
||||
c.mu.Unlock()
|
||||
|
||||
nm := &NetworkMap{
|
||||
nm := &netmap.NetworkMap{
|
||||
SelfNode: resp.Node,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
@@ -790,7 +745,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
}
|
||||
}
|
||||
addUserProfile(nm.User)
|
||||
magicDNSSuffix := nm.MagicDNSSuffix()
|
||||
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
|
||||
for _, peer := range resp.Peers {
|
||||
peer.InitDisplayNames(magicDNSSuffix)
|
||||
if !peer.Sharer.IsZero() {
|
||||
if c.keepSharerAndUserSplit {
|
||||
addUserProfile(peer.Sharer)
|
||||
@@ -972,19 +930,21 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
|
||||
var Debug = initDebug()
|
||||
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
use := os.Getenv("TS_DEBUG_USE_DISCO")
|
||||
return debug{
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1065,6 +1025,24 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
|
||||
if mapRes.PeerSeenChange != nil {
|
||||
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
||||
for _, n := range newFull {
|
||||
peerByID[n.ID] = n
|
||||
}
|
||||
now := time.Now()
|
||||
for nodeID, seen := range mapRes.PeerSeenChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
if seen {
|
||||
n.LastSeen = &now
|
||||
} else {
|
||||
n.LastSeen = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Persist; DO NOT EDIT.
|
||||
|
||||
package controlclient
|
||||
|
||||
import ()
|
||||
|
||||
// Clone makes a deep copy of Persist.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Persist) Clone() *Persist {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Persist)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -156,3 +157,15 @@ func TestNewDirect(t *testing.T) {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
@@ -709,10 +709,19 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
|
||||
m, err = client.Recv()
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
if c.isClosed() {
|
||||
err = ErrClientClosed
|
||||
}
|
||||
}
|
||||
return m, connGen, err
|
||||
}
|
||||
|
||||
func (c *Client) isClosed() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.closed
|
||||
}
|
||||
|
||||
// Close closes the client. It will not automatically reconnect after
|
||||
// being closed.
|
||||
func (c *Client) Close() error {
|
||||
|
||||
@@ -5,20 +5,32 @@
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// RunWatchConnectionLoop loops forever, sending WatchConnectionChanges and subscribing to
|
||||
// RunWatchConnectionLoop loops until ctx is done, sending WatchConnectionChanges and subscribing to
|
||||
// connection changes.
|
||||
//
|
||||
// If the server's public key is ignoreServerKey, RunWatchConnectionLoop returns.
|
||||
//
|
||||
// Otherwise, the add and remove funcs are called as clients come & go.
|
||||
func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove func(key.Public)) {
|
||||
//
|
||||
// infoLogf, if non-nil, is the logger to write periodic status
|
||||
// updates about how many peers are on the server. Error log output is
|
||||
// set to the c's logger, regardless of infoLogf's value.
|
||||
//
|
||||
// To force RunWatchConnectionLoop to return quickly, its ctx needs to
|
||||
// be closed, and c itself needs to be closed.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.Public, infoLogf logger.Logf, add, remove func(key.Public)) {
|
||||
if infoLogf == nil {
|
||||
infoLogf = logger.Discard
|
||||
}
|
||||
logf := c.logf
|
||||
const retryInterval = 5 * time.Second
|
||||
const statusInterval = 10 * time.Second
|
||||
@@ -45,7 +57,7 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
|
||||
if loggedConnected {
|
||||
return
|
||||
}
|
||||
logf("connected; %d peers", len(present))
|
||||
infoLogf("connected; %d peers", len(present))
|
||||
loggedConnected = true
|
||||
}
|
||||
|
||||
@@ -79,12 +91,21 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
sleep := func(d time.Duration) {
|
||||
t := time.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
}
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
err := c.WatchConnectionChanges()
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("WatchConnectionChanges: %v", err)
|
||||
time.Sleep(retryInterval)
|
||||
sleep(retryInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -97,7 +118,7 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("Recv: %v", err)
|
||||
time.Sleep(retryInterval)
|
||||
sleep(retryInterval)
|
||||
break
|
||||
}
|
||||
if connGen != lastConnGen {
|
||||
@@ -114,9 +135,8 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
|
||||
}
|
||||
if now := time.Now(); now.Sub(lastStatus) > statusInterval {
|
||||
lastStatus = now
|
||||
logf("%d peers", len(present))
|
||||
infoLogf("%d peers", len(present))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func Parse(p []byte) (Message, error) {
|
||||
case TypePong:
|
||||
return parsePong(ver, p)
|
||||
case TypeCallMeMaybe:
|
||||
return CallMeMaybe{}, nil
|
||||
return parseCallMeMaybe(ver, p)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
|
||||
}
|
||||
@@ -122,13 +122,57 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
|
||||
//
|
||||
// The recipient may choose to not open a path back, if it's already
|
||||
// happy with its path. But usually it will.
|
||||
type CallMeMaybe struct{}
|
||||
type CallMeMaybe struct {
|
||||
// MyNumber is what the peer believes its endpoints are.
|
||||
//
|
||||
// Prior to Tailscale 1.4, the endpoints were exchanged purely
|
||||
// between nodes and the control server.
|
||||
//
|
||||
// Starting with Tailscale 1.4, clients advertise their endpoints.
|
||||
// Older clients won't use this, but newer clients should
|
||||
// use any endpoints in here that aren't included from control.
|
||||
//
|
||||
// Control might have sent stale endpoints if the client was idle
|
||||
// before contacting us. In that case, the client likely did a STUN
|
||||
// request immediately before sending the CallMeMaybe to recreate
|
||||
// their NAT port mapping, and that new good endpoint is included
|
||||
// in this field, but might not yet be in control's endpoints.
|
||||
// (And in the future, control will stop distributing endpoints
|
||||
// when clients are suitably new.)
|
||||
MyNumber []netaddr.IPPort
|
||||
}
|
||||
|
||||
func (CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0)
|
||||
const epLength = 16 + 2 // 16 byte IP address + 2 byte port
|
||||
|
||||
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
|
||||
for _, ipp := range m.MyNumber {
|
||||
a := ipp.IP.As16()
|
||||
copy(p[:], a[:])
|
||||
binary.BigEndian.PutUint16(p[16:], ipp.Port)
|
||||
p = p[epLength:]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
|
||||
m = new(CallMeMaybe)
|
||||
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
m.MyNumber = make([]netaddr.IPPort, 0, len(p)/epLength)
|
||||
for len(p) > 0 {
|
||||
var a [16]byte
|
||||
copy(a[:], p)
|
||||
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
|
||||
IP: netaddr.IPFrom16(a),
|
||||
Port: binary.BigEndian.Uint16(p[16:18]),
|
||||
})
|
||||
p = p[epLength:]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Pong is a response a Ping.
|
||||
//
|
||||
// It includes the sender's source IP + port, so it's effectively a
|
||||
@@ -171,7 +215,7 @@ func MessageSummary(m Message) string {
|
||||
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
|
||||
case *Pong:
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case CallMeMaybe:
|
||||
case *CallMeMaybe:
|
||||
return "call-me-maybe"
|
||||
default:
|
||||
return fmt.Sprintf("%#v", m)
|
||||
|
||||
@@ -44,9 +44,19 @@ func TestMarshalAndParse(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe",
|
||||
m: CallMeMaybe{},
|
||||
m: &CallMeMaybe{},
|
||||
want: "03 00",
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe_endpoints",
|
||||
m: &CallMeMaybe{
|
||||
MyNumber: []netaddr.IPPort{
|
||||
netaddr.MustParseIPPort("1.2.3.4:567"),
|
||||
netaddr.MustParseIPPort("[2001::3456]:789"),
|
||||
},
|
||||
},
|
||||
want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
8
go.mod
8
go.mod
@@ -24,15 +24,15 @@ require (
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742
|
||||
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
|
||||
20
go.sum
20
go.sum
@@ -294,6 +294,20 @@ github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551 h1:hjBVxvVa
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4Ui+lzHm6gOq7s2Oe4ksxkbUYtS/JuoJ2Nce8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3 h1:wpgSErXul2ysBGZVVM0fKISMgZ9BZRXuOYAyn8MxAbY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8 h1:7OWHhbjWEuEjt+VlgOXLC4+iPkAvwTMU4zASxa+mKbw=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365 h1:0OC8+fnUCx5ww7uRSlzbcVC6Q/FK0PmVclmimbpWbyk=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210204220812-81c7f3687020 h1:DbQtiKont9TyOBIuTHhj1UUpWE75QcsyBiJPxTbqRGQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210204220812-81c7f3687020/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210209210853-838c6fc0dc12 h1:kk8nOHkXmG/yD1a4FQvH7+VOdNEP7GKkQimXFR2iwv8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210209210853-838c6fc0dc12/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210160038-385d6fdeb57a h1:zgMmUGUb2U3E9VerpED4MlIceYjTT0QgpGr3qJKHyBE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210160038-385d6fdeb57a/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 h1:VzTS7LIwCH8jlxwrZguU0TsCLV/MDOunoNIDJdFajyM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
@@ -336,6 +350,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -386,6 +402,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2l
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -441,6 +459,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001 h1:/dSxr6gT0FNI1MO5WLJo8mTmItROeOKTkDn+7OwWBos=
|
||||
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestDeepPrint(t *testing.T) {
|
||||
|
||||
@@ -9,12 +9,11 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
type State int
|
||||
@@ -46,10 +45,10 @@ func (s State) String() string {
|
||||
|
||||
// EngineStatus contains WireGuard engine stats.
|
||||
type EngineStatus struct {
|
||||
RBytes, WBytes wgengine.ByteCount
|
||||
RBytes, WBytes int64
|
||||
NumLive int
|
||||
LiveDERPs int // number of active DERP connections
|
||||
LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus
|
||||
LivePeers map[tailcfg.NodeKey]ipnstate.PeerStatusLite
|
||||
}
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
@@ -59,16 +58,16 @@ type EngineStatus struct {
|
||||
// They are JSON-encoded on the wire, despite the lack of struct tags.
|
||||
type Notify struct {
|
||||
_ structs.Incomparable
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *controlclient.NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *netmap.NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
PingResult *ipnstate.PingResult
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
type FakeBackend struct {
|
||||
@@ -54,7 +54,7 @@ func (b *FakeBackend) login() {
|
||||
b.newState(NeedsMachineAuth)
|
||||
b.newState(Stopped)
|
||||
// TODO(apenwarr): Fill in a more interesting netmap here.
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
|
||||
b.newState(Starting)
|
||||
// TODO(apenwarr): Fill in a more interesting status.
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
@@ -92,7 +92,7 @@ func (b *FakeBackend) RequestStatus() {
|
||||
}
|
||||
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Ping(ip string) {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
@@ -22,7 +22,7 @@ type Handle struct {
|
||||
|
||||
// Mutex protects everything below
|
||||
mu sync.Mutex
|
||||
netmapCache *controlclient.NetworkMap
|
||||
netmapCache *netmap.NetworkMap
|
||||
engineStatusCache EngineStatus
|
||||
stateCache State
|
||||
prefsCache *Prefs
|
||||
@@ -129,7 +129,7 @@ func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
|
||||
return []netaddr.IPPrefix{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *controlclient.NetworkMap {
|
||||
func (h *Handle) NetMap() *netmap.NetworkMap {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/internal/deepprint"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/net/interfaces"
|
||||
@@ -29,6 +29,8 @@ import (
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
@@ -37,6 +39,8 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wgcfg/nmcfg"
|
||||
)
|
||||
|
||||
var controlDebugFlags = getControlDebugFlags()
|
||||
@@ -66,7 +70,7 @@ type LocalBackend struct {
|
||||
keyLogf logger.Logf // for printing list of peers on change
|
||||
statsLogf logger.Logf // for printing peers stats on change
|
||||
e wgengine.Engine
|
||||
store StateStore
|
||||
store ipn.StateStore
|
||||
backendLogID string
|
||||
portpoll *portlist.Poller // may be nil
|
||||
portpollOnce sync.Once // guards starting readPoller
|
||||
@@ -78,20 +82,21 @@ type LocalBackend struct {
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
notify func(Notify)
|
||||
notify func(ipn.Notify)
|
||||
c *controlclient.Client
|
||||
stateKey StateKey // computed in part from user-provided value
|
||||
userID string // current controlling user ID (for Windows, primarily)
|
||||
prefs *Prefs
|
||||
stateKey ipn.StateKey // computed in part from user-provided value
|
||||
userID string // current controlling user ID (for Windows, primarily)
|
||||
prefs *ipn.Prefs
|
||||
inServerMode bool
|
||||
machinePrivKey wgkey.Private
|
||||
state State
|
||||
state ipn.State
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
netMap *controlclient.NetworkMap
|
||||
netMap *netmap.NetworkMap
|
||||
nodeByAddr map[netaddr.IP]*tailcfg.Node
|
||||
activeLogin string // last logged LoginName from netMap
|
||||
engineStatus EngineStatus
|
||||
engineStatus ipn.EngineStatus
|
||||
endpoints []string
|
||||
blocked bool
|
||||
authURL string
|
||||
@@ -106,7 +111,7 @@ type LocalBackend struct {
|
||||
|
||||
// NewLocalBackend returns a new LocalBackend that is ready to run,
|
||||
// but is not actually running.
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengine.Engine) (*LocalBackend, error) {
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
|
||||
if e == nil {
|
||||
panic("ipn.NewLocalBackend: wgengine must not be nil")
|
||||
}
|
||||
@@ -129,7 +134,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
|
||||
e: e,
|
||||
store: store,
|
||||
backendLogID: logid,
|
||||
state: NoState,
|
||||
state: ipn.NoState,
|
||||
portpoll: portpoll,
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
}
|
||||
@@ -150,7 +155,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
|
||||
networkUp := ifst.AnyInterfaceUp()
|
||||
if b.c != nil {
|
||||
go b.c.SetPaused(b.state == Stopped || !networkUp)
|
||||
go b.c.SetPaused(b.state == ipn.Stopped || !networkUp)
|
||||
}
|
||||
|
||||
// If the PAC-ness of the network changed, reconfig wireguard+route to
|
||||
@@ -158,7 +163,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
if hadPAC != ifst.HasPAC() {
|
||||
b.logf("linkChange: in state %v; PAC changed from %v->%v", b.state, hadPAC, ifst.HasPAC())
|
||||
switch b.state {
|
||||
case NoState, Stopped:
|
||||
case ipn.NoState, ipn.Stopped:
|
||||
// Do nothing.
|
||||
default:
|
||||
go b.authReconfig()
|
||||
@@ -231,10 +236,26 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WhoIs reports the node and user who owns the node with the given IP.
|
||||
// If ok == true, n and u are valid.
|
||||
func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
n, ok = b.nodeByAddr[ip]
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
}
|
||||
u, ok = b.netMap.UserProfiles[n.User]
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
}
|
||||
return n, u, true
|
||||
}
|
||||
|
||||
// SetDecompressor sets a decompression function, which must be a zstd
|
||||
@@ -264,7 +285,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// Auth completed, unblock the engine
|
||||
b.blockEngineUpdates(false)
|
||||
b.authReconfig()
|
||||
b.send(Notify{LoginFinished: &empty.Message{}})
|
||||
b.send(ipn.Notify{LoginFinished: &empty.Message{}})
|
||||
}
|
||||
|
||||
prefsChanged := false
|
||||
@@ -289,13 +310,15 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
prefsChanged = true
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
if b.keepOneExitNodeLocked(st.NetMap) {
|
||||
prefsChanged = true
|
||||
}
|
||||
b.setNetMapLocked(st.NetMap)
|
||||
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.authURL = st.URL
|
||||
}
|
||||
if b.state == NeedsLogin {
|
||||
if b.state == ipn.NeedsLogin {
|
||||
if !b.prefs.WantRunning {
|
||||
prefsChanged = true
|
||||
}
|
||||
@@ -315,7 +338,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
}
|
||||
b.send(Notify{Prefs: prefs})
|
||||
b.send(ipn.Notify{Prefs: prefs})
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
if netMap != nil {
|
||||
@@ -334,7 +357,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
}
|
||||
b.e.SetDERPMap(st.NetMap.DERPMap)
|
||||
|
||||
b.send(Notify{NetMap: st.NetMap})
|
||||
b.send(ipn.Notify{NetMap: st.NetMap})
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.logf("Received auth URL: %.20v...", st.URL)
|
||||
@@ -348,6 +371,53 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
b.authReconfig()
|
||||
}
|
||||
|
||||
// keepOneExitNodeLocked edits nm to retain only the default
|
||||
// routes provided by the exit node specified in b.prefs. It returns
|
||||
// whether prefs was mutated as part of the process, due to an exit
|
||||
// node IP being converted into a node ID.
|
||||
func (b *LocalBackend) keepOneExitNodeLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
// If we have a desired IP on file, try to find the corresponding
|
||||
// node.
|
||||
if !b.prefs.ExitNodeIP.IsZero() {
|
||||
// IP takes precedence over ID, so if both are set, clear ID.
|
||||
if b.prefs.ExitNodeID != "" {
|
||||
b.prefs.ExitNodeID = ""
|
||||
prefsChanged = true
|
||||
}
|
||||
|
||||
peerLoop:
|
||||
for _, peer := range nm.Peers {
|
||||
for _, addr := range peer.Addresses {
|
||||
if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP {
|
||||
continue
|
||||
}
|
||||
// Found the node being referenced, upgrade prefs to
|
||||
// reference it directly for next time.
|
||||
b.prefs.ExitNodeID = peer.StableID
|
||||
b.prefs.ExitNodeIP = netaddr.IP{}
|
||||
prefsChanged = true
|
||||
break peerLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we have a node ID if the requested node is in
|
||||
// the netmap. If not, the ID will be empty, and we'll strip out
|
||||
// all default routes.
|
||||
for _, peer := range nm.Peers {
|
||||
out := peer.AllowedIPs[:0]
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Bits == 0 && peer.StableID != b.prefs.ExitNodeID {
|
||||
continue
|
||||
}
|
||||
out = append(out, allowedIP)
|
||||
}
|
||||
peer.AllowedIPs = out
|
||||
}
|
||||
|
||||
return prefsChanged
|
||||
}
|
||||
|
||||
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
|
||||
// This updates the endpoints both in the backend and in the control client.
|
||||
func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
@@ -376,7 +446,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
b.statusChanged.Broadcast()
|
||||
b.statusLock.Unlock()
|
||||
|
||||
b.send(Notify{Engine: &es})
|
||||
b.send(ipn.Notify{Engine: &es})
|
||||
}
|
||||
|
||||
// Start applies the configuration specified in opts, and starts the
|
||||
@@ -389,7 +459,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
// guarantee that switching from one user's state to another is
|
||||
// actually a supported operation (it should be, but it's very unclear
|
||||
// from the following whether or not that is a safe transition).
|
||||
func (b *LocalBackend) Start(opts Options) error {
|
||||
func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
if opts.Prefs == nil && opts.StateKey == "" {
|
||||
return errors.New("no state key or prefs provided")
|
||||
}
|
||||
@@ -422,7 +492,7 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
hostinfo.NetInfo = b.hostinfo.NetInfo
|
||||
}
|
||||
b.hostinfo = hostinfo
|
||||
b.state = NoState
|
||||
b.state = ipn.NoState
|
||||
|
||||
if err := b.loadStateLocked(opts.StateKey, opts.Prefs, opts.LegacyConfigPath); err != nil {
|
||||
b.mu.Unlock()
|
||||
@@ -440,7 +510,7 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
|
||||
b.notify = opts.Notify
|
||||
b.setNetMapLocked(nil)
|
||||
persist := b.prefs.Persist
|
||||
persistv := b.prefs.Persist
|
||||
machinePrivKey := b.machinePrivKey
|
||||
b.mu.Unlock()
|
||||
|
||||
@@ -473,14 +543,14 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
}
|
||||
|
||||
var err error
|
||||
if persist == nil {
|
||||
if persistv == nil {
|
||||
// let controlclient initialize it
|
||||
persist = &controlclient.Persist{}
|
||||
persistv = &persist.Persist{}
|
||||
}
|
||||
cli, err := controlclient.New(controlclient.Options{
|
||||
MachinePrivateKey: machinePrivKey,
|
||||
Logf: logger.WithPrefix(b.logf, "control: "),
|
||||
Persist: *persist,
|
||||
Persist: *persistv,
|
||||
ServerURL: b.serverURL,
|
||||
AuthKey: opts.AuthKey,
|
||||
Hostinfo: hostinfo,
|
||||
@@ -519,8 +589,8 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
|
||||
blid := b.backendLogID
|
||||
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
|
||||
b.send(Notify{BackendLogID: &blid})
|
||||
b.send(Notify{Prefs: prefs})
|
||||
b.send(ipn.Notify{BackendLogID: &blid})
|
||||
b.send(ipn.Notify{Prefs: prefs})
|
||||
|
||||
cli.Login(nil, controlclient.LoginDefault)
|
||||
return nil
|
||||
@@ -528,7 +598,7 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
|
||||
// updateFilter updates the packet filter in wgengine based on the
|
||||
// given netMap and user preferences.
|
||||
func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Prefs) {
|
||||
func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs) {
|
||||
// NOTE(danderson): keep change detection as the first thing in
|
||||
// this function. Don't try to optimize by returning early, more
|
||||
// likely than not you'll just end up breaking the change
|
||||
@@ -562,12 +632,13 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
|
||||
localNets := unmapIPPrefixes(netMap.Addresses, advRoutes)
|
||||
|
||||
oldFilter := b.e.GetFilter()
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(b.logf))
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, oldFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v", packetFilter)
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, oldFilter, b.logf))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +657,7 @@ func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool {
|
||||
// dnsMapsEqual determines whether the new and the old network map
|
||||
// induce the same DNS map. It does so without allocating memory,
|
||||
// at the expense of giving false negatives if peers are reordered.
|
||||
func dnsMapsEqual(new, old *controlclient.NetworkMap) bool {
|
||||
func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
|
||||
if (old == nil) != (new == nil) {
|
||||
return false
|
||||
}
|
||||
@@ -620,7 +691,7 @@ func dnsMapsEqual(new, old *controlclient.NetworkMap) bool {
|
||||
|
||||
// updateDNSMap updates the domain map in the DNS resolver in wgengine
|
||||
// based on the given netMap and user preferences.
|
||||
func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
func (b *LocalBackend) updateDNSMap(netMap *netmap.NetworkMap) {
|
||||
if netMap == nil {
|
||||
b.logf("dns map: (not ready)")
|
||||
return
|
||||
@@ -684,7 +755,7 @@ func (b *LocalBackend) readPoller() {
|
||||
|
||||
// send delivers n to the connected frontend. If no frontend is
|
||||
// connected, the notification is dropped without being delivered.
|
||||
func (b *LocalBackend) send(n Notify) {
|
||||
func (b *LocalBackend) send(n ipn.Notify) {
|
||||
b.mu.Lock()
|
||||
notify := b.notify
|
||||
b.mu.Unlock()
|
||||
@@ -710,9 +781,9 @@ func (b *LocalBackend) popBrowserAuthNow() {
|
||||
|
||||
b.blockEngineUpdates(true)
|
||||
b.stopEngineAndWait()
|
||||
b.send(Notify{BrowseToURL: &url})
|
||||
if b.State() == Running {
|
||||
b.enterState(Starting)
|
||||
b.send(ipn.Notify{BrowseToURL: &url})
|
||||
if b.State() == ipn.Running {
|
||||
b.enterState(ipn.Starting)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,21 +814,21 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
|
||||
}
|
||||
|
||||
keyText, err := b.store.ReadState(MachineKeyStateKey)
|
||||
keyText, err := b.store.ReadState(ipn.MachineKeyStateKey)
|
||||
if err == nil {
|
||||
if err := b.machinePrivKey.UnmarshalText(keyText); err != nil {
|
||||
return fmt.Errorf("invalid key in %s key of %v: %w", MachineKeyStateKey, b.store, err)
|
||||
return fmt.Errorf("invalid key in %s key of %v: %w", ipn.MachineKeyStateKey, b.store, err)
|
||||
}
|
||||
if b.machinePrivKey.IsZero() {
|
||||
return fmt.Errorf("invalid zero key stored in %v key of %v", MachineKeyStateKey, b.store)
|
||||
return fmt.Errorf("invalid zero key stored in %v key of %v", ipn.MachineKeyStateKey, b.store)
|
||||
}
|
||||
if !legacyMachineKey.IsZero() && !bytes.Equal(legacyMachineKey[:], b.machinePrivKey[:]) {
|
||||
b.logf("frontend-provided legacy machine key ignored; used value from server state")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != ErrStateNotExist {
|
||||
return fmt.Errorf("error reading %v key of %v: %w", MachineKeyStateKey, b.store, err)
|
||||
if err != ipn.ErrStateNotExist {
|
||||
return fmt.Errorf("error reading %v key of %v: %w", ipn.MachineKeyStateKey, b.store, err)
|
||||
}
|
||||
|
||||
// If we didn't find one already on disk and the prefs already
|
||||
@@ -780,7 +851,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
}
|
||||
|
||||
keyText, _ = b.machinePrivKey.MarshalText()
|
||||
if err := b.store.WriteState(MachineKeyStateKey, keyText); err != nil {
|
||||
if err := b.store.WriteState(ipn.MachineKeyStateKey, keyText); err != nil {
|
||||
b.logf("error writing machine key to store: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -793,14 +864,14 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
// user and prefs. If userID is blank or prefs is blank, no work is done.
|
||||
//
|
||||
// b.mu may either be held or not.
|
||||
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
|
||||
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *ipn.Prefs) {
|
||||
if userID == "" || prefs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if prefs.ForceDaemon {
|
||||
stateKey := StateKey("user-" + userID)
|
||||
if err := b.store.WriteState(ServerModeStartKey, []byte(stateKey)); err != nil {
|
||||
stateKey := ipn.StateKey("user-" + userID)
|
||||
if err := b.store.WriteState(ipn.ServerModeStartKey, []byte(stateKey)); err != nil {
|
||||
b.logf("WriteState error: %v", err)
|
||||
}
|
||||
// It's important we do this here too, even if it looks
|
||||
@@ -812,7 +883,7 @@ func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
|
||||
b.logf("WriteState error: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := b.store.WriteState(ServerModeStartKey, nil); err != nil {
|
||||
if err := b.store.WriteState(ipn.ServerModeStartKey, nil); err != nil {
|
||||
b.logf("WriteState error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -821,7 +892,7 @@ func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
|
||||
// loadStateLocked sets b.prefs and b.stateKey based on a complex
|
||||
// combination of key, prefs, and legacyPath. b.mu must be held when
|
||||
// calling.
|
||||
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) (err error) {
|
||||
func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs, legacyPath string) (err error) {
|
||||
if prefs == nil && key == "" {
|
||||
panic("state key and prefs are both unset")
|
||||
}
|
||||
@@ -863,19 +934,19 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
||||
b.logf("using backend prefs")
|
||||
bs, err := b.store.ReadState(key)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrStateNotExist) {
|
||||
if errors.Is(err, ipn.ErrStateNotExist) {
|
||||
if legacyPath != "" {
|
||||
b.prefs, err = LoadPrefs(legacyPath)
|
||||
b.prefs, err = ipn.LoadPrefs(legacyPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
b.logf("failed to load legacy prefs: %v", err)
|
||||
}
|
||||
b.prefs = NewPrefs()
|
||||
b.prefs = ipn.NewPrefs()
|
||||
} else {
|
||||
b.logf("imported prefs from relaynode for %q: %v", key, b.prefs.Pretty())
|
||||
}
|
||||
} else {
|
||||
b.prefs = NewPrefs()
|
||||
b.prefs = ipn.NewPrefs()
|
||||
b.logf("created empty state for %q: %s", key, b.prefs.Pretty())
|
||||
}
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
@@ -885,7 +956,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
||||
}
|
||||
return fmt.Errorf("store.ReadState(%q): %v", key, err)
|
||||
}
|
||||
b.prefs, err = PrefsFromBytes(bs, false)
|
||||
b.prefs, err = ipn.PrefsFromBytes(bs, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("PrefsFromBytes: %v", err)
|
||||
}
|
||||
@@ -897,7 +968,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
||||
}
|
||||
|
||||
// State returns the backend state machine's current state.
|
||||
func (b *LocalBackend) State() State {
|
||||
func (b *LocalBackend) State() ipn.State {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
@@ -913,7 +984,7 @@ func (b *LocalBackend) InServerMode() bool {
|
||||
// getEngineStatus returns a copy of b.engineStatus.
|
||||
//
|
||||
// TODO(bradfitz): remove this and use Status() throughout.
|
||||
func (b *LocalBackend) getEngineStatus() EngineStatus {
|
||||
func (b *LocalBackend) getEngineStatus() ipn.EngineStatus {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
@@ -969,7 +1040,7 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
|
||||
mapCopy.Expiry = time.Now().Add(x)
|
||||
}
|
||||
b.setNetMapLocked(&mapCopy)
|
||||
b.send(Notify{NetMap: b.netMap})
|
||||
b.send(ipn.Notify{NetMap: b.netMap})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Ping(ipStr string) {
|
||||
@@ -979,7 +1050,7 @@ func (b *LocalBackend) Ping(ipStr string) {
|
||||
return
|
||||
}
|
||||
b.e.Ping(ip, func(pr *ipnstate.PingResult) {
|
||||
b.send(Notify{PingResult: pr})
|
||||
b.send(ipn.Notify{PingResult: pr})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -988,11 +1059,11 @@ func (b *LocalBackend) Ping(ipStr string) {
|
||||
// b.mu must be held; mostly because the caller is about to anyway, and doing so
|
||||
// gives us slightly better guarantees about the two peers stats lines not
|
||||
// being intermixed if there are concurrent calls to our caller.
|
||||
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret EngineStatus) {
|
||||
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret ipn.EngineStatus) {
|
||||
var peerStats, peerKeys strings.Builder
|
||||
|
||||
ret.LiveDERPs = s.DERPs
|
||||
ret.LivePeers = map[tailcfg.NodeKey]wgengine.PeerStatus{}
|
||||
ret.LivePeers = map[tailcfg.NodeKey]ipnstate.PeerStatusLite{}
|
||||
for _, p := range s.Peers {
|
||||
if !p.LastHandshake.IsZero() {
|
||||
fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes)
|
||||
@@ -1048,7 +1119,7 @@ func (b *LocalBackend) SetWantRunning(wantRunning bool) {
|
||||
|
||||
// SetPrefs saves new user preferences and propagates them throughout
|
||||
// the system. Implements Backend.
|
||||
func (b *LocalBackend) SetPrefs(newp *Prefs) {
|
||||
func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
if newp == nil {
|
||||
panic("SetPrefs got nil prefs")
|
||||
}
|
||||
@@ -1115,7 +1186,7 @@ func (b *LocalBackend) SetPrefs(newp *Prefs) {
|
||||
b.authReconfig()
|
||||
}
|
||||
|
||||
b.send(Notify{Prefs: newp})
|
||||
b.send(ipn.Notify{Prefs: newp})
|
||||
}
|
||||
|
||||
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
|
||||
@@ -1141,7 +1212,7 @@ func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
|
||||
|
||||
// NetMap returns the latest cached network map received from
|
||||
// controlclient, or nil if no network map was received yet.
|
||||
func (b *LocalBackend) NetMap() *controlclient.NetworkMap {
|
||||
func (b *LocalBackend) NetMap() *netmap.NetworkMap {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.netMap
|
||||
@@ -1183,23 +1254,21 @@ func (b *LocalBackend) authReconfig() {
|
||||
return
|
||||
}
|
||||
|
||||
var flags controlclient.WGConfigFlags
|
||||
var flags netmap.WGConfigFlags
|
||||
if uc.RouteAll {
|
||||
flags |= controlclient.AllowDefaultRoute
|
||||
// TODO(apenwarr): Make subnet routes a different pref?
|
||||
flags |= controlclient.AllowSubnetRoutes
|
||||
flags |= netmap.AllowSubnetRoutes
|
||||
}
|
||||
if uc.AllowSingleHosts {
|
||||
flags |= controlclient.AllowSingleHosts
|
||||
flags |= netmap.AllowSingleHosts
|
||||
}
|
||||
if hasPAC && disableSubnetsIfPAC {
|
||||
if flags&controlclient.AllowSubnetRoutes != 0 {
|
||||
if flags&netmap.AllowSubnetRoutes != 0 {
|
||||
b.logf("authReconfig: have PAC; disabling subnet routes")
|
||||
flags &^= controlclient.AllowSubnetRoutes
|
||||
flags &^= netmap.AllowSubnetRoutes
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := nm.WGCfg(b.logf, flags)
|
||||
cfg, err := nmcfg.WGCfg(nm, b.logf, flags)
|
||||
if err != nil {
|
||||
b.logf("wgcfg: %v", err)
|
||||
return
|
||||
@@ -1231,15 +1300,20 @@ func (b *LocalBackend) authReconfig() {
|
||||
|
||||
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
|
||||
// Each entry has a trailing period.
|
||||
func magicDNSRootDomains(nm *controlclient.NetworkMap) []string {
|
||||
func magicDNSRootDomains(nm *netmap.NetworkMap) []string {
|
||||
if v := nm.MagicDNSSuffix(); v != "" {
|
||||
return []string{strings.Trim(v, ".") + "."}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ipv4Default = netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||
ipv6Default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
|
||||
func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
|
||||
func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
|
||||
rs := &router.Config{
|
||||
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
|
||||
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes),
|
||||
@@ -1251,6 +1325,32 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
|
||||
rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...)
|
||||
}
|
||||
|
||||
// Sanity check: we expect the control server to program both a v4
|
||||
// and a v6 default route, if default routing is on. Fill in
|
||||
// blackhole routes appropriately if we're missing some. This is
|
||||
// likely to break some functionality, but if the user expressed a
|
||||
// preference for routing remotely, we want to avoid leaking
|
||||
// traffic at the expense of functionality.
|
||||
if prefs.ExitNodeID != "" || !prefs.ExitNodeIP.IsZero() {
|
||||
var default4, default6 bool
|
||||
for _, route := range rs.Routes {
|
||||
if route == ipv4Default {
|
||||
default4 = true
|
||||
} else if route == ipv6Default {
|
||||
default6 = true
|
||||
}
|
||||
if default4 && default6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !default4 {
|
||||
rs.Routes = append(rs.Routes, ipv4Default)
|
||||
}
|
||||
if !default6 {
|
||||
rs.Routes = append(rs.Routes, ipv6Default)
|
||||
}
|
||||
}
|
||||
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
|
||||
IP: tsaddr.TailscaleServiceIP(),
|
||||
Bits: 32,
|
||||
@@ -1268,7 +1368,7 @@ func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
|
||||
return ret
|
||||
}
|
||||
|
||||
func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) {
|
||||
func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
|
||||
if h := prefs.Hostname; h != "" {
|
||||
hi.Hostname = h
|
||||
}
|
||||
@@ -1288,7 +1388,7 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) {
|
||||
// places twiddle IPN internal state without going through here, so
|
||||
// really this is more "one of several places in which random things
|
||||
// happen".
|
||||
func (b *LocalBackend) enterState(newState State) {
|
||||
func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
b.mu.Lock()
|
||||
state := b.state
|
||||
b.state = newState
|
||||
@@ -1306,19 +1406,19 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v)",
|
||||
state, newState, prefs.WantRunning)
|
||||
if notify != nil {
|
||||
b.send(Notify{State: &newState})
|
||||
b.send(ipn.Notify{State: &newState})
|
||||
}
|
||||
|
||||
if bc != nil {
|
||||
bc.SetPaused(newState == Stopped || !networkUp)
|
||||
bc.SetPaused(newState == ipn.Stopped || !networkUp)
|
||||
}
|
||||
|
||||
switch newState {
|
||||
case NeedsLogin:
|
||||
case ipn.NeedsLogin:
|
||||
systemd.Status("Needs login: %s", authURL)
|
||||
b.blockEngineUpdates(true)
|
||||
fallthrough
|
||||
case Stopped:
|
||||
case ipn.Stopped:
|
||||
err := b.e.Reconfig(&wgcfg.Config{}, &router.Config{})
|
||||
if err != nil {
|
||||
b.logf("Reconfig(down): %v", err)
|
||||
@@ -1327,11 +1427,11 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
if authURL == "" {
|
||||
systemd.Status("Stopped; run 'tailscale up' to log in")
|
||||
}
|
||||
case Starting, NeedsMachineAuth:
|
||||
case ipn.Starting, ipn.NeedsMachineAuth:
|
||||
b.authReconfig()
|
||||
// Needed so that UpdateEndpoints can run
|
||||
b.e.RequestStatus()
|
||||
case Running:
|
||||
case ipn.Running:
|
||||
var addrs []string
|
||||
for _, addr := range b.netMap.Addresses {
|
||||
addrs = append(addrs, addr.IP.String())
|
||||
@@ -1345,7 +1445,7 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
|
||||
// nextState returns the state the backend seems to be in, based on
|
||||
// its internal state.
|
||||
func (b *LocalBackend) nextState() State {
|
||||
func (b *LocalBackend) nextState() ipn.State {
|
||||
b.mu.Lock()
|
||||
b.assertClientLocked()
|
||||
var (
|
||||
@@ -1361,31 +1461,31 @@ func (b *LocalBackend) nextState() State {
|
||||
if c.AuthCantContinue() {
|
||||
// Auth was interrupted or waiting for URL visit,
|
||||
// so it won't proceed without human help.
|
||||
return NeedsLogin
|
||||
return ipn.NeedsLogin
|
||||
} else {
|
||||
// Auth or map request needs to finish
|
||||
return state
|
||||
}
|
||||
case !wantRunning:
|
||||
return Stopped
|
||||
return ipn.Stopped
|
||||
case !netMap.Expiry.IsZero() && time.Until(netMap.Expiry) <= 0:
|
||||
return NeedsLogin
|
||||
return ipn.NeedsLogin
|
||||
case netMap.MachineStatus != tailcfg.MachineAuthorized:
|
||||
// TODO(crawshaw): handle tailcfg.MachineInvalid
|
||||
return NeedsMachineAuth
|
||||
case state == NeedsMachineAuth:
|
||||
return ipn.NeedsMachineAuth
|
||||
case state == ipn.NeedsMachineAuth:
|
||||
// (if we get here, we know MachineAuthorized == true)
|
||||
return Starting
|
||||
case state == Starting:
|
||||
return ipn.Starting
|
||||
case state == ipn.Starting:
|
||||
if st := b.getEngineStatus(); st.NumLive > 0 || st.LiveDERPs > 0 {
|
||||
return Running
|
||||
return ipn.Running
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
case state == Running:
|
||||
return Running
|
||||
case state == ipn.Running:
|
||||
return ipn.Running
|
||||
default:
|
||||
return Starting
|
||||
return ipn.Starting
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1397,7 +1497,7 @@ func (b *LocalBackend) RequestEngineStatus() {
|
||||
// RequestStatus implements Backend.
|
||||
func (b *LocalBackend) RequestStatus() {
|
||||
st := b.Status()
|
||||
b.send(Notify{Status: st})
|
||||
b.send(ipn.Notify{Status: st})
|
||||
}
|
||||
|
||||
// stateMachine updates the state machine state based on other things
|
||||
@@ -1493,7 +1593,7 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
||||
c.SetNetInfo(ni)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
|
||||
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
var login string
|
||||
if nm != nil {
|
||||
login = nm.UserProfiles[nm.User].LoginName
|
||||
@@ -1506,6 +1606,39 @@ func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
|
||||
b.logf("active login: %v", login)
|
||||
b.activeLogin = login
|
||||
}
|
||||
|
||||
if nm == nil {
|
||||
b.nodeByAddr = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Update the nodeByAddr index.
|
||||
if b.nodeByAddr == nil {
|
||||
b.nodeByAddr = map[netaddr.IP]*tailcfg.Node{}
|
||||
}
|
||||
// First pass, mark everything unwanted.
|
||||
for k := range b.nodeByAddr {
|
||||
b.nodeByAddr[k] = nil
|
||||
}
|
||||
addNode := func(n *tailcfg.Node) {
|
||||
for _, ipp := range n.Addresses {
|
||||
if ipp.IsSingleIP() {
|
||||
b.nodeByAddr[ipp.IP] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
if nm.SelfNode != nil {
|
||||
addNode(nm.SelfNode)
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
addNode(p)
|
||||
}
|
||||
// Third pass, actually delete the unwanted items.
|
||||
for k, v := range b.nodeByAddr {
|
||||
if v == nil {
|
||||
delete(b.nodeByAddr, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnlyPublicKeys returns the current machine and node public
|
||||
@@ -2,13 +2,14 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tailcfg"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
func TestNetworkMapCompare(t *testing.T) {
|
||||
@@ -26,7 +27,7 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b *controlclient.NetworkMap
|
||||
a, b *netmap.NetworkMap
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
@@ -37,76 +38,76 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"b nil",
|
||||
&controlclient.NetworkMap{},
|
||||
&netmap.NetworkMap{},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"a nil",
|
||||
nil,
|
||||
&controlclient.NetworkMap{},
|
||||
&netmap.NetworkMap{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"both default",
|
||||
&controlclient.NetworkMap{},
|
||||
&controlclient.NetworkMap{},
|
||||
&netmap.NetworkMap{},
|
||||
&netmap.NetworkMap{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"names identical",
|
||||
&controlclient.NetworkMap{Name: "map1"},
|
||||
&controlclient.NetworkMap{Name: "map1"},
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"names differ",
|
||||
&controlclient.NetworkMap{Name: "map1"},
|
||||
&controlclient.NetworkMap{Name: "map2"},
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
&netmap.NetworkMap{Name: "map2"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Peers identical",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Peer list length",
|
||||
// length of Peers list differs
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{{}}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node lists identical",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node lists differ",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node Users differ",
|
||||
// User field is not checked.
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
@@ -2,18 +2,20 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
@@ -38,9 +40,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
idA := logid(0xaa)
|
||||
|
||||
// set up a LocalBackend, super bare bones. No functional data.
|
||||
store := &MemoryStore{
|
||||
cache: make(map[StateKey][]byte),
|
||||
}
|
||||
store := &ipn.MemoryStore{}
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -53,7 +53,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
defer lb.Shutdown()
|
||||
|
||||
// custom adjustments for required non-nil fields
|
||||
lb.prefs = NewPrefs()
|
||||
lb.prefs = ipn.NewPrefs()
|
||||
lb.hostinfo = &tailcfg.Hostinfo{}
|
||||
// hacky manual override of the usual log-on-change behaviour of keylogf
|
||||
lb.keyLogf = logListen.Logf
|
||||
@@ -67,8 +67,8 @@ func TestLocalLogLines(t *testing.T) {
|
||||
}
|
||||
|
||||
// log prefs line
|
||||
persist := &controlclient.Persist{}
|
||||
prefs := NewPrefs()
|
||||
persist := &persist.Persist{}
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.Persist = persist
|
||||
lb.SetPrefs(prefs)
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
|
||||
// log peers, peer keys
|
||||
status := &wgengine.Status{
|
||||
Peers: []wgengine.PeerStatus{wgengine.PeerStatus{
|
||||
Peers: []ipnstate.PeerStatusLite{{
|
||||
TxBytes: 10,
|
||||
RxBytes: 10,
|
||||
LastHandshake: time.Now(),
|
||||
@@ -24,9 +24,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
@@ -91,7 +94,7 @@ type Options struct {
|
||||
// server is an IPN backend and its set of 0 or more active connections
|
||||
// talking to an IPN backend.
|
||||
type server struct {
|
||||
b *ipn.LocalBackend
|
||||
b *ipnlocal.LocalBackend
|
||||
logf logger.Logf
|
||||
// resetOnZero is whether to call bs.Reset on transition from
|
||||
// 1->0 connections. That is, this is whether the backend is
|
||||
@@ -113,10 +116,11 @@ type server struct {
|
||||
|
||||
// connIdentity represents the owner of a localhost TCP connection.
|
||||
type connIdentity struct {
|
||||
Unknown bool
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
Unknown bool
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
IsUnixSock bool
|
||||
}
|
||||
|
||||
// getConnIdentity returns the localhost TCP connection's identity information
|
||||
@@ -125,7 +129,9 @@ type connIdentity struct {
|
||||
// to be able to map it and couldn't.
|
||||
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
||||
return connIdentity{Unknown: true}, nil
|
||||
ci = connIdentity{Unknown: true}
|
||||
_, ci.IsUnixSock = c.(*net.UnixConn)
|
||||
return ci, nil
|
||||
}
|
||||
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
|
||||
if err != nil {
|
||||
@@ -216,13 +222,22 @@ func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
|
||||
}
|
||||
}
|
||||
|
||||
// bufferHasHTTPRequest reports whether br looks like it has an HTTP
|
||||
// request in it, without reading any bytes from it.
|
||||
func bufferHasHTTPRequest(br *bufio.Reader) bool {
|
||||
peek, _ := br.Peek(br.Buffered())
|
||||
return mem.HasPrefix(mem.B(peek), mem.S("GET ")) ||
|
||||
mem.HasPrefix(mem.B(peek), mem.S("POST ")) ||
|
||||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
|
||||
}
|
||||
|
||||
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
// First see if it's an HTTP request.
|
||||
br := bufio.NewReader(c)
|
||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||
peek, _ := br.Peek(4)
|
||||
br.Peek(4)
|
||||
c.SetReadDeadline(time.Time{})
|
||||
isHTTPReq := string(peek) == "GET "
|
||||
isHTTPReq := bufferHasHTTPRequest(br)
|
||||
|
||||
ci, err := s.addConn(c, isHTTPReq)
|
||||
if err != nil {
|
||||
@@ -249,7 +264,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
s.b.SetCurrentUserID(ci.UserID)
|
||||
|
||||
if isHTTPReq {
|
||||
httpServer := http.Server{
|
||||
httpServer := &http.Server{
|
||||
// Localhost connections are cheap; so only do
|
||||
// keep-alives for a short period of time, as these
|
||||
// active connections lock the server into only serving
|
||||
@@ -503,41 +518,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
}()
|
||||
logf("Listening on %v", listen.Addr())
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
break
|
||||
}
|
||||
logf("ipnserver%d: getEngine failed again: %v", i, err)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var store ipn.StateStore
|
||||
if opts.StatePath != "" {
|
||||
store, err = ipn.NewFileStore(opts.StatePath)
|
||||
@@ -566,7 +546,83 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
store = &ipn.MemoryStore{}
|
||||
}
|
||||
|
||||
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
|
||||
// Issue 1187: on Windows, in unattended mode,
|
||||
// sometimes we try 5 times and fail to create the
|
||||
// engine before the system's ready. Hack until the
|
||||
// bug if fixed properly: if we're running in
|
||||
// unattended mode on Windows, keep trying forever,
|
||||
// waiting for the machine to be ready (networking to
|
||||
// come up?) and then dial our own safesocket TCP
|
||||
// listener to wake up the usual mechanism that lets
|
||||
// us surface getEngine errors to UI clients. (We
|
||||
// don't want to just call getEngine in a loop without
|
||||
// the listener.Accept, as we do want to handle client
|
||||
// connections so we can tell them about errors)
|
||||
|
||||
bootRaceWaitForEngine, bootRaceWaitForEngineCancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
if runtime.GOOS == "windows" && opts.AutostartStateKey != "" {
|
||||
logf("ipnserver: in unattended mode, waiting for engine availability")
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
// Wait for it to be ready.
|
||||
go func() {
|
||||
defer bootRaceWaitForEngineCancel()
|
||||
t0 := time.Now()
|
||||
for {
|
||||
time.Sleep(10 * time.Second)
|
||||
if _, err := getEngine(); err != nil {
|
||||
logf("ipnserver: unattended mode engine load: %v", err)
|
||||
continue
|
||||
}
|
||||
c, err := net.Dial("tcp", listen.Addr().String())
|
||||
logf("ipnserver: engine created after %v; waking up Accept: Dial error: %v", time.Since(t0).Round(time.Second), err)
|
||||
if err == nil {
|
||||
c.Close()
|
||||
}
|
||||
break
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
bootRaceWaitForEngineCancel()
|
||||
}
|
||||
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
<-bootRaceWaitForEngine.Done()
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
break
|
||||
}
|
||||
logf("ipnserver%d: getEngine failed again: %v", i, err)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -579,6 +635,9 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
|
||||
serveHTMLStatus(w, b)
|
||||
})
|
||||
h := localapi.NewHandler(b)
|
||||
h.PermitRead = true
|
||||
opts.DebugMux.Handle("/localapi/", h)
|
||||
}
|
||||
|
||||
server.b = b
|
||||
@@ -756,6 +815,27 @@ func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
|
||||
return func() (wgengine.Engine, error) { return eng, nil }
|
||||
}
|
||||
|
||||
// getEngineUntilItWorksWrapper returns a getEngine wrapper that does
|
||||
// not call getEngine concurrently and stops calling getEngine once
|
||||
// it's returned a working engine.
|
||||
func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, error)) func() (wgengine.Engine, error) {
|
||||
var mu sync.Mutex
|
||||
var engGood wgengine.Engine
|
||||
return func() (wgengine.Engine, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if engGood != nil {
|
||||
return engGood, nil
|
||||
}
|
||||
e, err := getEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engGood = e
|
||||
return e, nil
|
||||
}
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
@@ -798,6 +878,13 @@ func (psc *protoSwitchConn) Close() error {
|
||||
|
||||
func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ci.IsUnixSock && strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
h := localapi.NewHandler(s.b)
|
||||
h.PermitRead = true
|
||||
h.PermitWrite = false // TODO: flesh out connIdentity on more platforms then set this
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if ci.Unknown {
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||
return
|
||||
@@ -806,7 +893,7 @@ func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) {
|
||||
func serveHTMLStatus(w http.ResponseWriter, b *ipnlocal.LocalBackend) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := b.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
|
||||
@@ -21,14 +21,21 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// Status represents the entire state of the IPN network.
|
||||
type Status struct {
|
||||
BackendState string
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
MagicDNSSuffix string // e.g. "userfoo.tailscale.net" (no surrounding dots)
|
||||
BackendState string
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
// MagicDNSSuffix is the network's MagicDNS suffix for nodes
|
||||
// in the network such as "userfoo.tailscale.net".
|
||||
// There are no surrounding dots.
|
||||
// MagicDNSSuffix should be populated regardless of whether a domain
|
||||
// has MagicDNS enabled.
|
||||
MagicDNSSuffix string
|
||||
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
@@ -43,6 +50,12 @@ func (s *Status) Peers() []key.Public {
|
||||
return kk
|
||||
}
|
||||
|
||||
type PeerStatusLite struct {
|
||||
TxBytes, RxBytes int64
|
||||
LastHandshake time.Time
|
||||
NodeKey tailcfg.NodeKey
|
||||
}
|
||||
|
||||
type PeerStatus struct {
|
||||
PublicKey key.Public
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
@@ -64,6 +77,7 @@ type PeerStatus struct {
|
||||
LastSeen time.Time // last seen to tailcontrol
|
||||
LastHandshake time.Time // with local wireguard
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
|
||||
// ShareeNode indicates this node exists in the netmap because
|
||||
// it's owned by a shared-to user and that node might connect
|
||||
@@ -231,6 +245,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if st.KeepAlive {
|
||||
e.KeepAlive = true
|
||||
}
|
||||
if st.ExitNode {
|
||||
e.ExitNode = true
|
||||
}
|
||||
if st.ShareeNode {
|
||||
e.ShareeNode = true
|
||||
}
|
||||
@@ -274,13 +291,22 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
|
||||
|
||||
f("<table>\n<thead>\n")
|
||||
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
|
||||
f("<tr><th>Peer</th><th>OS</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Connection</th></tr>\n")
|
||||
f("</thead>\n<tbody>\n")
|
||||
|
||||
now := time.Now()
|
||||
|
||||
var peers []*PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
SortPeers(peers)
|
||||
|
||||
for _, ps := range peers {
|
||||
var actAgo string
|
||||
if !ps.LastWrite.IsZero() {
|
||||
ago := now.Sub(ps.LastWrite)
|
||||
@@ -296,40 +322,44 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
owner = owner[:i]
|
||||
}
|
||||
}
|
||||
f("<tr><td>%s</td><td>%s %s<br><span class=\"tailaddr\">%s</span></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
|
||||
peer.ShortString(),
|
||||
html.EscapeString(ps.SimpleHostName()),
|
||||
|
||||
hostName := ps.SimpleHostName()
|
||||
dnsName := strings.TrimRight(ps.DNSName, ".")
|
||||
if i := strings.Index(dnsName, "."); i != -1 && dnsname.HasSuffix(dnsName, st.MagicDNSSuffix) {
|
||||
dnsName = dnsName[:i]
|
||||
}
|
||||
if strings.EqualFold(dnsName, hostName) || ps.UserID != st.Self.UserID {
|
||||
hostName = ""
|
||||
}
|
||||
var hostNameHTML string
|
||||
if hostName != "" {
|
||||
hostNameHTML = "<br>" + html.EscapeString(hostName)
|
||||
}
|
||||
|
||||
f("<tr><td>%s</td><td class=acenter>%s</td>"+
|
||||
"<td><b>%s</b>%s<div class=\"tailaddr\">%s</div></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
|
||||
ps.PublicKey.ShortString(),
|
||||
osEmoji(ps.OS),
|
||||
html.EscapeString(dnsName),
|
||||
hostNameHTML,
|
||||
ps.TailAddr,
|
||||
html.EscapeString(owner),
|
||||
ps.RxBytes,
|
||||
ps.TxBytes,
|
||||
actAgo,
|
||||
)
|
||||
f("<td class=\"aright\">")
|
||||
f("<td>")
|
||||
|
||||
// TODO: let server report this active bool instead
|
||||
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
relay := ps.Relay
|
||||
if relay != "" {
|
||||
if active && ps.CurAddr == "" {
|
||||
f("🔗 <b>derp-%v</b><br>", html.EscapeString(relay))
|
||||
} else {
|
||||
f("derp-%v<br>", html.EscapeString(relay))
|
||||
if active {
|
||||
if ps.Relay != "" && ps.CurAddr == "" {
|
||||
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct <b>%s</b>", html.EscapeString(ps.CurAddr))
|
||||
}
|
||||
}
|
||||
|
||||
match := false
|
||||
for _, addr := range ps.Addrs {
|
||||
if addr == ps.CurAddr {
|
||||
match = true
|
||||
f("🔗 <b>%s</b><br>", addr)
|
||||
} else {
|
||||
f("%s<br>", addr)
|
||||
}
|
||||
}
|
||||
if ps.CurAddr != "" && !match {
|
||||
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>", ps.CurAddr)
|
||||
}
|
||||
f("</td>") // end Addrs
|
||||
|
||||
f("</tr>\n")
|
||||
@@ -375,3 +405,17 @@ type PingResult struct {
|
||||
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
func SortPeers(peers []*PeerStatus) {
|
||||
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
|
||||
}
|
||||
|
||||
func sortKey(ps *PeerStatus) string {
|
||||
if ps.DNSName != "" {
|
||||
return ps.DNSName
|
||||
}
|
||||
if ps.HostName != "" {
|
||||
return ps.HostName
|
||||
}
|
||||
return ps.TailAddr
|
||||
}
|
||||
|
||||
95
ipn/localapi/localapi.go
Normal file
95
ipn/localapi/localapi.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package localapi contains the HTTP server handlers for tailscaled's API server.
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func NewHandler(b *ipnlocal.LocalBackend) *Handler {
|
||||
return &Handler{b: b}
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
// RequiredPassword, if non-empty, forces all HTTP
|
||||
// requests to have HTTP basic auth with this password.
|
||||
// It's used by the sandboxed macOS sameuserproof GUI auth mechanism.
|
||||
RequiredPassword string
|
||||
|
||||
// PermitRead is whether read-only HTTP handlers are allowed.
|
||||
PermitRead bool
|
||||
|
||||
// PermitWrite is whether mutating HTTP handlers are allowed.
|
||||
PermitWrite bool
|
||||
|
||||
b *ipnlocal.LocalBackend
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if h.b == nil {
|
||||
http.Error(w, "server has no local backend", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.RequiredPassword != "" {
|
||||
_, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
http.Error(w, "auth required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if pass != h.RequiredPassword {
|
||||
http.Error(w, "bad password", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
h.serveWhoIs(w, r)
|
||||
default:
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "whois access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
b := h.b
|
||||
var ip netaddr.IP
|
||||
if v := r.FormValue("ip"); v != "" {
|
||||
var err error
|
||||
ip, err = netaddr.ParseIP(r.FormValue("ip"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'ip' parameter", 400)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "missing 'ip' parameter", 400)
|
||||
return
|
||||
}
|
||||
n, u, ok := b.WhoIs(ip)
|
||||
if !ok {
|
||||
http.Error(w, "no match for IP", 404)
|
||||
return
|
||||
}
|
||||
res := &tailcfg.WhoIsResponse{
|
||||
Node: n,
|
||||
UserProfile: &u,
|
||||
}
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
@@ -146,6 +146,10 @@ func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
}
|
||||
|
||||
// ErrMsgPermissionDenied is the Notify.ErrMessage value used an
|
||||
// operation was done from a user/context that didn't have permission.
|
||||
const ErrMsgPermissionDenied = "permission denied"
|
||||
|
||||
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
|
||||
@@ -178,7 +182,7 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
}
|
||||
|
||||
if IsReadonlyContext(ctx) {
|
||||
msg := "permission denied"
|
||||
msg := ErrMsgPermissionDenied
|
||||
bs.send(Notify{ErrMessage: &msg})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ import (
|
||||
|
||||
func TestReadWrite(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
err := WriteMsg(&buf, []byte("Test string1"))
|
||||
@@ -64,9 +62,7 @@ func TestReadWrite(t *testing.T) {
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
b := &FakeBackend{}
|
||||
var bs *BackendServer
|
||||
|
||||
39
ipn/prefs.go
39
ipn/prefs.go
@@ -17,8 +17,9 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
@@ -28,8 +29,10 @@ type Prefs struct {
|
||||
// ControlURL is the URL of the control server to use.
|
||||
ControlURL string
|
||||
|
||||
// RouteAll specifies whether to accept subnet and default routes
|
||||
// advertised by other nodes on the Tailscale network.
|
||||
// RouteAll specifies whether to accept subnets advertised by
|
||||
// other nodes on the Tailscale network. Note that this does not
|
||||
// include default routes (0.0.0.0/0 and ::/0), those are
|
||||
// controlled by ExitNodeID/IP below.
|
||||
RouteAll bool
|
||||
|
||||
// AllowSingleHosts specifies whether to install routes for each
|
||||
@@ -44,6 +47,24 @@ type Prefs struct {
|
||||
// packets stop flowing. What's up with that?
|
||||
AllowSingleHosts bool
|
||||
|
||||
// ExitNodeID and ExitNodeIP specify the node that should be used
|
||||
// as an exit node for internet traffic. At most one of these
|
||||
// should be non-zero.
|
||||
//
|
||||
// The preferred way to express the chosen node is ExitNodeID, but
|
||||
// in some cases it's not possible to use that ID (e.g. in the
|
||||
// linux CLI, before tailscaled has a netmap). For those
|
||||
// situations, we allow specifying the exit node by IP, and
|
||||
// ipnlocal.LocalBackend will translate the IP into an ID when the
|
||||
// node is found in the netmap.
|
||||
//
|
||||
// If the selected exit node doesn't exist (e.g. it's not part of
|
||||
// the current tailnet), or it doesn't offer exit node services, a
|
||||
// blackhole route will be installed on the local system to
|
||||
// prevent any traffic escaping to the local network.
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netaddr.IP
|
||||
|
||||
// CorpDNS specifies whether to install the Tailscale network's
|
||||
// DNS configuration, if it exists.
|
||||
CorpDNS bool
|
||||
@@ -116,14 +137,14 @@ type Prefs struct {
|
||||
|
||||
// NetfilterMode specifies how much to manage netfilter rules for
|
||||
// Tailscale, if at all.
|
||||
NetfilterMode router.NetfilterMode
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
||||
// We can maybe do that once we're sure which module should persist
|
||||
// it (backend or frontend?)
|
||||
Persist *controlclient.Persist `json:"Config"`
|
||||
Persist *persist.Persist `json:"Config"`
|
||||
}
|
||||
|
||||
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
|
||||
@@ -191,6 +212,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.ControlURL == p2.ControlURL &&
|
||||
p.RouteAll == p2.RouteAll &&
|
||||
p.AllowSingleHosts == p2.AllowSingleHosts &&
|
||||
p.ExitNodeID == p2.ExitNodeID &&
|
||||
p.ExitNodeIP == p2.ExitNodeIP &&
|
||||
p.CorpDNS == p2.CorpDNS &&
|
||||
p.WantRunning == p2.WantRunning &&
|
||||
p.NotepadURLs == p2.NotepadURLs &&
|
||||
@@ -240,7 +263,7 @@ func NewPrefs() *Prefs {
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: true,
|
||||
NetfilterMode: router.NetfilterOn,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +275,7 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) {
|
||||
if len(b) == 0 {
|
||||
return p, nil
|
||||
}
|
||||
persist := &controlclient.Persist{}
|
||||
persist := &persist.Persist{}
|
||||
err := json.Unmarshal(b, persist)
|
||||
if err == nil && (persist.Provider != "" || persist.LoginName != "") {
|
||||
// old-style relaynode config; import it
|
||||
|
||||
@@ -8,8 +8,9 @@ package ipn
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Prefs.
|
||||
@@ -23,7 +24,7 @@ func (src *Prefs) Clone() *Prefs {
|
||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||
if dst.Persist != nil {
|
||||
dst.Persist = new(controlclient.Persist)
|
||||
dst.Persist = new(persist.Persist)
|
||||
*dst.Persist = *src.Persist
|
||||
}
|
||||
return dst
|
||||
@@ -35,6 +36,8 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
ControlURL string
|
||||
RouteAll bool
|
||||
AllowSingleHosts bool
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netaddr.IP
|
||||
CorpDNS bool
|
||||
WantRunning bool
|
||||
ShieldsUp bool
|
||||
@@ -46,6 +49,6 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
ForceDaemon bool
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
NoSNAT bool
|
||||
NetfilterMode router.NetfilterMode
|
||||
Persist *controlclient.Persist
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
@@ -30,7 +30,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
func TestPrefsEqual(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, prefsHandles)
|
||||
@@ -99,6 +99,28 @@ func TestPrefsEqual(t *testing.T) {
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{ExitNodeID: "n1234"},
|
||||
&Prefs{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{ExitNodeID: "n1234"},
|
||||
&Prefs{ExitNodeID: "n1234"},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
||||
&Prefs{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
||||
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{CorpDNS: true},
|
||||
&Prefs{CorpDNS: false},
|
||||
@@ -192,24 +214,24 @@ func TestPrefsEqual(t *testing.T) {
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{NetfilterMode: router.NetfilterOff},
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
&Prefs{NetfilterMode: preftype.NetfilterOff},
|
||||
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
||||
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{Persist: &controlclient.Persist{}},
|
||||
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
|
||||
&Prefs{Persist: &persist.Persist{}},
|
||||
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
|
||||
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
|
||||
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
||||
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
@@ -274,7 +296,7 @@ func TestBasicPrefs(t *testing.T) {
|
||||
func TestPrefsPersist(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
c := controlclient.Persist{
|
||||
c := persist.Persist{
|
||||
LoginName: "test@example.com",
|
||||
}
|
||||
p := Prefs{
|
||||
@@ -340,14 +362,14 @@ func TestPrefsPretty(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{},
|
||||
Persist: &persist.Persist{},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: wgkey.Private{1: 1},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -338,6 +339,18 @@ func New(collection string) *Policy {
|
||||
tryFixLogStateLocation(dir, cmdName)
|
||||
|
||||
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
|
||||
|
||||
// The Windows service previously ran as tailscale-ipn.exe, so
|
||||
// let's keep using that log base name if it exists.
|
||||
if runtime.GOOS == "windows" && cmdName == "tailscaled" {
|
||||
const oldCmdName = "tailscale-ipn"
|
||||
oldPath := filepath.Join(dir, oldCmdName+".log.conf")
|
||||
if fi, err := os.Stat(oldPath); err == nil && fi.Mode().IsRegular() {
|
||||
cfgPath = oldPath
|
||||
cmdName = oldCmdName
|
||||
}
|
||||
}
|
||||
|
||||
var oldc *Config
|
||||
data, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
@@ -387,6 +400,13 @@ func New(collection string) *Policy {
|
||||
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
|
||||
}
|
||||
|
||||
if val, ok := os.LookupEnv("TS_LOG_TARGET"); ok {
|
||||
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
|
||||
c.BaseURL = val
|
||||
u, _ := url.Parse(val)
|
||||
c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)}
|
||||
}
|
||||
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
|
||||
@@ -131,11 +131,11 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
path1 := filePrefix + ".log1.txt"
|
||||
path2 := filePrefix + ".log2.txt"
|
||||
|
||||
f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0600)
|
||||
f1, err = openFileSync(path1, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0600)
|
||||
f2, err = openFileSync(path2, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
27
logtail/filch/openfilesync_darwin.go
Normal file
27
logtail/filch/openfilesync_darwin.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//+build darwin
|
||||
|
||||
package filch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func openFileSync(path string, flag int, perm os.FileMode) (*os.File, error) {
|
||||
f, err := os.OpenFile(path, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = unix.FcntlInt(uintptr(f.Fd()), syscall.F_NOCACHE, 1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openFileSync: %w", err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
16
logtail/filch/openfilesync_notdarwin.go
Normal file
16
logtail/filch/openfilesync_notdarwin.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//+build !darwin
|
||||
|
||||
package filch
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func openFileSync(path string, flag int, perm os.FileMode) (*os.File, error) {
|
||||
// TODO(crawshaw): on Linux and FreeBSD, use O_SYNC
|
||||
return os.OpenFile(path, flag, perm)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -39,8 +40,11 @@ func Tailscale() (net.IP, *net.Interface, error) {
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok && IsTailscaleIP(ipnet.IP) {
|
||||
return ipnet.IP, &iface, nil
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netaddr.FromStdIP(ipnet.IP)
|
||||
if ok && tsaddr.IsTailscaleIP(nip) {
|
||||
return ipnet.IP, &iface, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,16 +61,21 @@ func maybeTailscaleInterfaceName(s string) bool {
|
||||
strings.HasPrefix(s, "utun")
|
||||
}
|
||||
|
||||
// IsTailscaleIP reports whether ip is an IP in a range used by
|
||||
// Tailscale virtual network interfaces.
|
||||
func IsTailscaleIP(ip net.IP) bool {
|
||||
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
|
||||
return tsaddr.IsTailscaleIP(nip)
|
||||
}
|
||||
|
||||
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
|
||||
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
|
||||
|
||||
func isProblematicInterface(nif *net.Interface) bool {
|
||||
name := nif.Name
|
||||
// Don't try to send disco/etc packets over zerotier; they effectively
|
||||
// DoS each other by doing traffic amplification, both of them
|
||||
// preferring/trying to use each other for transport. See:
|
||||
// https://github.com/tailscale/tailscale/issues/1208
|
||||
if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LocalAddresses returns the machine's IP addresses, separated by
|
||||
// whether they're loopback addresses.
|
||||
func LocalAddresses() (regular, loopback []string, err error) {
|
||||
@@ -77,8 +86,10 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
}
|
||||
for i := range ifaces {
|
||||
iface := &ifaces[i]
|
||||
if !isUp(iface) {
|
||||
// Down interfaces don't count
|
||||
if !isUp(iface) || isProblematicInterface(iface) {
|
||||
// Skip down interfaces and ones that are
|
||||
// problematic that we don't want to try to
|
||||
// send Tailscale traffic over.
|
||||
continue
|
||||
}
|
||||
ifcIsLoopback := isLoopback(iface)
|
||||
@@ -186,10 +197,9 @@ func (s *State) String() string {
|
||||
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
|
||||
ifs := make([]string, 0, len(s.InterfaceUp))
|
||||
for k := range s.InterfaceUp {
|
||||
if allLoopbackIPs(s.InterfaceIPs[k]) {
|
||||
continue
|
||||
if anyInterestingIP(s.InterfaceIPs[k]) {
|
||||
ifs = append(ifs, k)
|
||||
}
|
||||
ifs = append(ifs, k)
|
||||
}
|
||||
sort.Slice(ifs, func(i, j int) bool {
|
||||
upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]]
|
||||
@@ -207,7 +217,7 @@ func (s *State) String() string {
|
||||
fmt.Fprintf(&sb, "%s:[", ifName)
|
||||
needSpace := false
|
||||
for _, ip := range s.InterfaceIPs[ifName] {
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
if !isInterestingIP(ip) {
|
||||
continue
|
||||
}
|
||||
if needSpace {
|
||||
@@ -392,14 +402,23 @@ var (
|
||||
v6Global1 = mustCIDR("2000::/3")
|
||||
)
|
||||
|
||||
func allLoopbackIPs(ips []netaddr.IP) bool {
|
||||
if len(ips) == 0 {
|
||||
return false
|
||||
}
|
||||
// anyInterestingIP reports ips contains any IP that matches
|
||||
// isInterestingIP.
|
||||
func anyInterestingIP(ips []netaddr.IP) bool {
|
||||
for _, ip := range ips {
|
||||
if !ip.IsLoopback() {
|
||||
return false
|
||||
if isInterestingIP(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isInterestingIP reports whether ip is an interesting IP that we
|
||||
// should log in interfaces.State logging. We don't need to show
|
||||
// localhost or link-local addresses.
|
||||
func isInterestingIP(ip netaddr.IP) bool {
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ package interfaces
|
||||
|
||||
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
|
||||
// Otherwise, it returns 0.
|
||||
int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
uint32_t privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
{
|
||||
// sockaddrs are after the message header
|
||||
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
|
||||
@@ -38,7 +38,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
return 0; // gateway not IPv4
|
||||
|
||||
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
|
||||
int ip;
|
||||
uint32_t ip;
|
||||
ip = gateway_si->sin_addr.s_addr;
|
||||
|
||||
unsigned char a, b;
|
||||
@@ -62,7 +62,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
// If no private gateway IP address was found, it returns 0.
|
||||
// On an error, it returns an error code in (0, 255].
|
||||
// Any private gateway IP address is > 255.
|
||||
int privateGatewayIP()
|
||||
uint32_t privateGatewayIP()
|
||||
{
|
||||
size_t needed;
|
||||
int mib[6];
|
||||
@@ -90,7 +90,7 @@ int privateGatewayIP()
|
||||
struct rt_msghdr2 *rtm;
|
||||
for (next = buf; next < lim; next += rtm->rtm_msglen) {
|
||||
rtm = (struct rt_msghdr2 *)next;
|
||||
int ip;
|
||||
uint32_t ip;
|
||||
ip = privateGatewayIPFromRoute(rtm);
|
||||
if (ip) {
|
||||
free(buf);
|
||||
|
||||
81
net/interfaces/interfaces_darwin_tailscaled.go
Normal file
81
net/interfaces/interfaces_darwin_tailscaled.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!redo,!ios
|
||||
// (Exclude redo, because we don't want this code in the App Store
|
||||
// version's sandbox, where it won't work, and also don't want it on
|
||||
// iOS. This is just for utun-using non-sandboxed cmd/tailscaled on macOS.
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
)
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
idx, err := DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
iface, err := net.InterfaceByIndex(idx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return iface.Name, nil
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
// Destination Gateway Flags Netif Expire
|
||||
// default 10.0.0.1 UGSc en0 <-- want this one
|
||||
// default 10.0.0.1 UGScI en1
|
||||
|
||||
// From man netstat:
|
||||
// U RTF_UP Route usable
|
||||
// G RTF_GATEWAY Destination requires forwarding by intermediary
|
||||
// S RTF_STATIC Manually added
|
||||
// c RTF_PRCLONING Protocol-specified generate new routes on use
|
||||
// I RTF_IFSCOPE Route is associated with an interface scope
|
||||
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
}
|
||||
17
net/interfaces/interfaces_default_route_test.go
Normal file
17
net/interfaces/interfaces_default_route_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux darwin,!redo
|
||||
|
||||
package interfaces
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultRouteInterface(t *testing.T) {
|
||||
v, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got %q", v)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!windows
|
||||
// +build !linux,!windows,!darwin darwin,redo
|
||||
|
||||
package interfaces
|
||||
|
||||
|
||||
@@ -5,30 +5,9 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsTailscaleIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"100.81.251.94", true},
|
||||
{"8.8.8.8", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ip := net.ParseIP(tt.ip)
|
||||
if ip == nil {
|
||||
t.Fatalf("failed to parse IP %q", tt.ip)
|
||||
}
|
||||
got := IsTailscaleIP(ip)
|
||||
if got != tt.want {
|
||||
t.Errorf("F(%q) = %v; want %v", tt.ip, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetState(t *testing.T) {
|
||||
st, err := GetState()
|
||||
if err != nil {
|
||||
|
||||
52
net/netns/netns_darwin_tailscaled.go
Normal file
52
net/netns/netns_darwin_tailscaled.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!redo
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
// control marks c as necessary to dial in a separate network namespace.
|
||||
//
|
||||
// It's intentionally the same signature as net.Dialer.Control
|
||||
// and net.ListenConfig.Control.
|
||||
func control(network, address string, c syscall.RawConn) error {
|
||||
if strings.HasPrefix(address, "127.") || address == "::1" {
|
||||
// Don't bind to an interface for localhost connections.
|
||||
return nil
|
||||
}
|
||||
idx, err := interfaces.DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
log.Printf("netns: DefaultRouteInterfaceIndex: %v", err)
|
||||
return nil
|
||||
}
|
||||
v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
|
||||
proto := unix.IPPROTO_IP
|
||||
opt := unix.IP_BOUND_IF
|
||||
if v6 {
|
||||
proto = unix.IPPROTO_IPV6
|
||||
opt = unix.IPV6_BOUND_IF
|
||||
}
|
||||
|
||||
var sockErr error
|
||||
err = c.Control(func(fd uintptr) {
|
||||
sockErr = unix.SetsockoptInt(int(fd), proto, opt, idx)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
|
||||
}
|
||||
if sockErr != nil {
|
||||
log.Printf("netns: control(%q, %q), v6=%v, index=%v: %v", network, address, v6, idx, sockErr)
|
||||
}
|
||||
return sockErr
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!windows
|
||||
// +build !linux,!windows,!darwin darwin,redo
|
||||
|
||||
package netns
|
||||
|
||||
|
||||
42
net/netns/netns_test.go
Normal file
42
net/netns/netns_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package netns contains the common code for using the Go net package
|
||||
// in a logical "network namespace" to avoid routing loops where
|
||||
// Tailscale-created packets would otherwise loop back through
|
||||
// Tailscale routes.
|
||||
//
|
||||
// Despite the name netns, the exact mechanism used differs by
|
||||
// operating system, and perhaps even by version of the OS.
|
||||
//
|
||||
// The netns package also handles connecting via SOCKS proxies when
|
||||
// configured by the environment.
|
||||
package netns
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var extNetwork = flag.Bool("use-external-network", false, "use the external network in tests")
|
||||
|
||||
func TestDial(t *testing.T) {
|
||||
if !*extNetwork {
|
||||
t.Skip("skipping test without --use-external-network")
|
||||
}
|
||||
d := NewDialer()
|
||||
c, err := d.Dial("tcp", "google.com:80")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
t.Logf("got addr %v", c.RemoteAddr())
|
||||
|
||||
c, err = d.Dial("tcp4", "google.com:80")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
t.Logf("got addr %v", c.RemoteAddr())
|
||||
}
|
||||
@@ -23,12 +23,14 @@ import (
|
||||
// Tailscale node has rejected the connection from another. Unlike a
|
||||
// TCP RST, this includes a reason.
|
||||
//
|
||||
// On the wire, after the IP header, it's currently 7 bytes:
|
||||
// On the wire, after the IP header, it's currently 7 or 8 bytes:
|
||||
// * '!'
|
||||
// * IPProto byte (IANA protocol number: TCP or UDP)
|
||||
// * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
|
||||
// * srcPort big endian uint16
|
||||
// * dstPort big endian uint16
|
||||
// * [optional] byte of flag bits:
|
||||
// lowest bit (0x1): MaybeBroken
|
||||
//
|
||||
// In the future it might also accept 16 byte IP flow src/dst IPs
|
||||
// after the header, if they're different than the IP-level ones.
|
||||
@@ -39,8 +41,21 @@ type TailscaleRejectedHeader struct {
|
||||
Dst netaddr.IPPort // rejected flow's dst
|
||||
Proto IPProto // proto that was rejected (TCP or UDP)
|
||||
Reason TailscaleRejectReason // why the connection was rejected
|
||||
|
||||
// MaybeBroken is whether the rejection is non-terminal (the
|
||||
// client should not fail immediately). This is sent by a
|
||||
// target when it's not sure whether it's totally broken, but
|
||||
// it might be. For example, the target tailscaled might think
|
||||
// its host firewall or IP forwarding aren't configured
|
||||
// properly, but tailscaled might be wrong (not having enough
|
||||
// visibility into what the OS is doing). When true, the
|
||||
// message is simply an FYI as a potential reason to use for
|
||||
// later when the pendopen connection tracking timer expires.
|
||||
MaybeBroken bool
|
||||
}
|
||||
|
||||
const rejectFlagBitMaybeBroken = 0x1
|
||||
|
||||
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
|
||||
return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst}
|
||||
}
|
||||
@@ -52,14 +67,32 @@ func (rh TailscaleRejectedHeader) String() string {
|
||||
type TSMPType uint8
|
||||
|
||||
const (
|
||||
// TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader.
|
||||
TSMPTypeRejectedConn TSMPType = '!'
|
||||
)
|
||||
|
||||
type TailscaleRejectReason byte
|
||||
|
||||
// IsZero reports whether r is the zero value, representing no rejection.
|
||||
func (r TailscaleRejectReason) IsZero() bool { return r == TailscaleRejectReasonNone }
|
||||
|
||||
const (
|
||||
RejectedDueToACLs TailscaleRejectReason = 'A'
|
||||
// TailscaleRejectReasonNone is the TailscaleRejectReason zero value.
|
||||
TailscaleRejectReasonNone TailscaleRejectReason = 0
|
||||
|
||||
// RejectedDueToACLs means that the host rejected the connection due to ACLs.
|
||||
RejectedDueToACLs TailscaleRejectReason = 'A'
|
||||
|
||||
// RejectedDueToShieldsUp means that the host rejected the connection due to shields being up.
|
||||
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
|
||||
|
||||
// RejectedDueToIPForwarding means that the relay node's IP
|
||||
// forwarding is disabled.
|
||||
RejectedDueToIPForwarding TailscaleRejectReason = 'F'
|
||||
|
||||
// RejectedDueToHostFirewall means that the target host's
|
||||
// firewall is blocking the traffic.
|
||||
RejectedDueToHostFirewall TailscaleRejectReason = 'W'
|
||||
)
|
||||
|
||||
func (r TailscaleRejectReason) String() string {
|
||||
@@ -68,22 +101,32 @@ func (r TailscaleRejectReason) String() string {
|
||||
return "acl"
|
||||
case RejectedDueToShieldsUp:
|
||||
return "shields"
|
||||
case RejectedDueToIPForwarding:
|
||||
return "host-ip-forwarding-unavailable"
|
||||
case RejectedDueToHostFirewall:
|
||||
return "host-firewall"
|
||||
}
|
||||
return fmt.Sprintf("0x%02x", byte(r))
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) hasFlags() bool {
|
||||
return h.MaybeBroken // the only one currently
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Len() int {
|
||||
var ipHeaderLen int
|
||||
if h.IPSrc.Is4() {
|
||||
ipHeaderLen = ip4HeaderLength
|
||||
} else if h.IPSrc.Is6() {
|
||||
ipHeaderLen = ip6HeaderLength
|
||||
}
|
||||
return ipHeaderLen +
|
||||
1 + // TSMPType byte
|
||||
v := 1 + // TSMPType byte
|
||||
1 + // IPProto byte
|
||||
1 + // TailscaleRejectReason byte
|
||||
2*2 // 2 uint16 ports
|
||||
if h.IPSrc.Is4() {
|
||||
v += ip4HeaderLength
|
||||
} else if h.IPSrc.Is6() {
|
||||
v += ip6HeaderLength
|
||||
}
|
||||
if h.hasFlags() {
|
||||
v++
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
@@ -117,6 +160,14 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
buf[2] = byte(h.Reason)
|
||||
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
|
||||
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
|
||||
|
||||
if h.hasFlags() {
|
||||
var flags byte
|
||||
if h.MaybeBroken {
|
||||
flags |= rejectFlagBitMaybeBroken
|
||||
}
|
||||
buf[7] = flags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -129,12 +180,17 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo
|
||||
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
|
||||
return
|
||||
}
|
||||
return TailscaleRejectedHeader{
|
||||
h = TailscaleRejectedHeader{
|
||||
Proto: IPProto(p[1]),
|
||||
Reason: TailscaleRejectReason(p[2]),
|
||||
IPSrc: pp.Src.IP,
|
||||
IPDst: pp.Dst.IP,
|
||||
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
|
||||
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
|
||||
}, true
|
||||
}
|
||||
if len(p) > 7 {
|
||||
flags := p[7]
|
||||
h.MaybeBroken = (flags & rejectFlagBitMaybeBroken) != 0
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
|
||||
@@ -37,6 +37,18 @@ func TestTailscaleRejectedHeader(t *testing.T) {
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields",
|
||||
},
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("2::2"),
|
||||
IPDst: netaddr.MustParseIP("1::1"),
|
||||
Src: netaddr.MustParseIPPort("[1::1]:567"),
|
||||
Dst: netaddr.MustParseIPPort("[2::2]:443"),
|
||||
Proto: UDP,
|
||||
Reason: RejectedDueToIPForwarding,
|
||||
MaybeBroken: true,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: host-ip-forwarding-unavailable",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
gotStr := tt.h.String()
|
||||
|
||||
@@ -59,6 +59,7 @@ func runSTUN(t *testing.T, pc net.PacketConn, stats *stunStats, done chan<- stru
|
||||
for {
|
||||
n, addr, err := pc.ReadFrom(buf[:])
|
||||
if err != nil {
|
||||
// TODO: when we switch to Go 1.16, replace this with errors.Is(err, net.ErrClosed)
|
||||
if strings.Contains(err.Error(), "closed network connection") {
|
||||
t.Logf("STUN server shutdown")
|
||||
return
|
||||
|
||||
@@ -8,6 +8,7 @@ package paths
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,9 @@ func DefaultTailscaledSocket() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ""
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
return "/var/run/tailscaled.socket"
|
||||
}
|
||||
if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() {
|
||||
return "/var/run/tailscale/tailscaled.sock"
|
||||
}
|
||||
@@ -42,5 +46,8 @@ func DefaultTailscaledStateFile() string {
|
||||
if f := stateFileFunc; f != nil {
|
||||
return f()
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ func statePath() string {
|
||||
return "/var/lib/tailscale/tailscaled.state"
|
||||
case "freebsd", "openbsd":
|
||||
return "/var/db/tailscale/tailscaled.state"
|
||||
case "darwin":
|
||||
return "/Library/Tailscale/tailscaled.state"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !darwin !arm64
|
||||
// +build go1.16,!ios !go1.16,!darwin !go1.16,!arm64
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows freebsd openbsd darwin,amd64
|
||||
// +build windows freebsd openbsd darwin,amd64 go1.16,darwin,arm64
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!amd64
|
||||
// +build go1.16,ios !go1.16,darwin,!amd64
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,amd64
|
||||
// +build darwin,amd64 go1.16,darwin,arm64
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGetList(t *testing.T) {
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
pl, err := GetList(nil)
|
||||
if err != nil {
|
||||
@@ -26,8 +25,7 @@ func TestGetList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIgnoreLocallyBoundPorts(t *testing.T) {
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestBasics(t *testing.T) {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
fmt.Printf("server read %d bytes.\n", n)
|
||||
t.Logf("server read %d bytes.", n)
|
||||
if string(b[:n]) != "world" {
|
||||
errs <- fmt.Errorf("got %#v, expected %#v\n", string(b[:n]), "world")
|
||||
return
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/paths"
|
||||
)
|
||||
|
||||
type closeable interface {
|
||||
@@ -27,6 +31,11 @@ func ConnCloseWrite(c net.Conn) error {
|
||||
return c.(closeable).CloseWrite()
|
||||
}
|
||||
|
||||
// ConnectDefault connects to the local Tailscale daemon.
|
||||
func ConnectDefault() (net.Conn, error) {
|
||||
return Connect(paths.DefaultTailscaledSocket(), 41112)
|
||||
}
|
||||
|
||||
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
|
||||
func Connect(path string, port uint16) (net.Conn, error) {
|
||||
return connect(path, port)
|
||||
@@ -38,3 +47,21 @@ func Connect(path string, port uint16) (net.Conn, error) {
|
||||
func Listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) {
|
||||
return listen(path, port)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTokenNotFound = errors.New("no token found")
|
||||
ErrNoTokenOnOS = errors.New("no token on " + runtime.GOOS)
|
||||
)
|
||||
|
||||
var localTCPPortAndToken func() (port int, token string, err error)
|
||||
|
||||
// LocalTCPPortAndToken returns the port number and auth token to connect to
|
||||
// the local Tailscale daemon. It's currently only applicable on macOS
|
||||
// when tailscaled is being run in the Mac Sandbox from the App Store version
|
||||
// of Tailscale.
|
||||
func LocalTCPPortAndToken() (port int, token string, err error) {
|
||||
if localTCPPortAndToken == nil {
|
||||
return 0, "", ErrNoTokenOnOS
|
||||
}
|
||||
return localTCPPortAndToken()
|
||||
}
|
||||
|
||||
52
safesocket/safesocket_darwin.go
Normal file
52
safesocket/safesocket_darwin.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
localTCPPortAndToken = localTCPPortAndTokenDarwin
|
||||
}
|
||||
|
||||
func localTCPPortAndTokenDarwin() (port int, token string, err error) {
|
||||
out, err := exec.Command("lsof",
|
||||
"-n", // numeric sockets; don't do DNS lookups, etc
|
||||
"-a", // logical AND remaining options
|
||||
fmt.Sprintf("-u%d", os.Getuid()), // process of same user only
|
||||
"-c", "IPNExtension", // starting with IPNExtension
|
||||
"-F", // machine-readable output
|
||||
).Output()
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("failed to run lsof looking for IPNExtension: %w", err)
|
||||
}
|
||||
bs := bufio.NewScanner(bytes.NewReader(out))
|
||||
subStr := []byte(".tailscale.ipn.macos/sameuserproof-")
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
i := bytes.Index(line, subStr)
|
||||
if i == -1 {
|
||||
continue
|
||||
}
|
||||
f := strings.SplitN(string(line[i+len(subStr):]), "-", 2)
|
||||
if len(f) != 2 {
|
||||
continue
|
||||
}
|
||||
portStr, token := f[0], f[1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("invalid port %q found in lsof", portStr)
|
||||
}
|
||||
return port, token, nil
|
||||
}
|
||||
return 0, "", ErrTokenNotFound
|
||||
}
|
||||
13
safesocket/safesocket_test.go
Normal file
13
safesocket/safesocket_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package safesocket
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLocalTCPPortAndToken(t *testing.T) {
|
||||
// Just test that it compiles for now (is available on all platforms).
|
||||
port, token, err := LocalTCPPortAndToken()
|
||||
t.Logf("got %v, %s, %v", port, token, err)
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -18,6 +16,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -56,18 +55,51 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
|
||||
c, err := net.Dial("unix", path)
|
||||
if err == nil {
|
||||
c.Close()
|
||||
if tailscaledRunningUnderLaunchd() {
|
||||
return nil, 0, fmt.Errorf("%v: address already in use; tailscaled already running under launchd (to stop, run: $ sudo launchctl stop com.tailscale.tailscaled)", path)
|
||||
}
|
||||
return nil, 0, fmt.Errorf("%v: address already in use", path)
|
||||
}
|
||||
_ = os.Remove(path)
|
||||
os.MkdirAll(filepath.Dir(path), 0755) // best effort
|
||||
|
||||
perm := socketPermissionsForOS()
|
||||
|
||||
sockDir := filepath.Dir(path)
|
||||
if _, err := os.Stat(sockDir); os.IsNotExist(err) {
|
||||
os.MkdirAll(sockDir, 0755) // best effort
|
||||
|
||||
// If we're on a platform where we want the socket
|
||||
// world-readable, open up the permissions on the
|
||||
// just-created directory too, in case a umask ate
|
||||
// it. This primarily affects running tailscaled by
|
||||
// hand as root in a shell, as there is no umask when
|
||||
// running under systemd.
|
||||
if perm == 0666 {
|
||||
if fi, err := os.Stat(sockDir); err == nil && fi.Mode()&0077 == 0 {
|
||||
if err := os.Chmod(sockDir, 0755); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pipe, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
os.Chmod(path, socketPermissionsForOS())
|
||||
os.Chmod(path, perm)
|
||||
return pipe, 0, err
|
||||
}
|
||||
|
||||
func tailscaledRunningUnderLaunchd() bool {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return false
|
||||
}
|
||||
plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
|
||||
_ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
|
||||
running := err == nil
|
||||
return running
|
||||
}
|
||||
|
||||
// socketPermissionsForOS returns the permissions to use for the
|
||||
// tailscaled.sock.
|
||||
func socketPermissionsForOS() os.FileMode {
|
||||
@@ -146,42 +178,24 @@ func connectMacOSAppSandbox() (net.Conn, error) {
|
||||
}
|
||||
f := strings.SplitN(best.Name(), "-", 3)
|
||||
portStr, token := f[1], f[2]
|
||||
return connectMacTCP(portStr, token)
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid port %q", portStr)
|
||||
}
|
||||
return connectMacTCP(port, token)
|
||||
}
|
||||
|
||||
// Otherwise, assume we're running the cmd/tailscale binary from outside the
|
||||
// App Sandbox.
|
||||
|
||||
out, err := exec.Command("lsof",
|
||||
"-n", // numeric sockets; don't do DNS lookups, etc
|
||||
"-a", // logical AND remaining options
|
||||
fmt.Sprintf("-u%d", os.Getuid()), // process of same user only
|
||||
"-c", "IPNExtension", // starting with IPNExtension
|
||||
"-F", // machine-readable output
|
||||
).Output()
|
||||
port, token, err := LocalTCPPortAndToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bs := bufio.NewScanner(bytes.NewReader(out))
|
||||
subStr := []byte(".tailscale.ipn.macos/sameuserproof-")
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
i := bytes.Index(line, subStr)
|
||||
if i == -1 {
|
||||
continue
|
||||
}
|
||||
f := strings.SplitN(string(line[i+len(subStr):]), "-", 2)
|
||||
if len(f) != 2 {
|
||||
continue
|
||||
}
|
||||
portStr, token := f[0], f[1]
|
||||
return connectMacTCP(portStr, token)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find Tailscale's IPNExtension process")
|
||||
return connectMacTCP(port, token)
|
||||
}
|
||||
|
||||
func connectMacTCP(portStr, token string) (net.Conn, error) {
|
||||
c, err := net.Dial("tcp", "localhost:"+portStr)
|
||||
func connectMacTCP(port int, token string) (net.Conn, error) {
|
||||
c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error dialing IPNExtension: %w", err)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// CurrentMapRequestVersion is the current MapRequest.Version value.
|
||||
@@ -32,7 +33,10 @@ import (
|
||||
// 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
|
||||
// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side
|
||||
// 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains
|
||||
const CurrentMapRequestVersion = 9
|
||||
// 10: 2021-01-17: client understands MapResponse.PeerSeenChange
|
||||
const CurrentMapRequestVersion = 10
|
||||
|
||||
type StableID string
|
||||
|
||||
type ID int64
|
||||
|
||||
@@ -54,6 +58,12 @@ func (u NodeID) IsZero() bool {
|
||||
return u == 0
|
||||
}
|
||||
|
||||
type StableNodeID StableID
|
||||
|
||||
func (u StableNodeID) IsZero() bool {
|
||||
return u == ""
|
||||
}
|
||||
|
||||
type GroupID ID
|
||||
|
||||
func (u GroupID) IsZero() bool {
|
||||
@@ -147,8 +157,9 @@ type UserProfile struct {
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID NodeID
|
||||
Name string // DNS
|
||||
ID NodeID
|
||||
StableID StableNodeID
|
||||
Name string // DNS
|
||||
|
||||
// User is the user who created the node. If ACL tags are in
|
||||
// use for the node then it doesn't reflect the ACL identity
|
||||
@@ -173,6 +184,98 @@ type Node struct {
|
||||
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
|
||||
|
||||
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
|
||||
|
||||
// The following three computed fields hold the various names that can
|
||||
// be used for this node in UIs. They are populated from controlclient
|
||||
// (not from control) by calling node.InitDisplayNames. These can be
|
||||
// used directly or accessed via node.DisplayName or node.DisplayNames.
|
||||
|
||||
ComputedName string `json:",omitempty"` // MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
|
||||
computedHostIfDifferent string // hostname, if different than ComputedName, otherwise empty
|
||||
ComputedNameWithHost string `json:",omitempty"` // either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set
|
||||
}
|
||||
|
||||
// DisplayName returns the user-facing name for a node which should
|
||||
// be shown in client UIs.
|
||||
//
|
||||
// Parameter forOwner specifies whether the name is requested by
|
||||
// the owner of the node. When forOwner is false, the hostname is
|
||||
// never included in the return value.
|
||||
//
|
||||
// Return value is either either "Name" or "Name (Hostname)", where
|
||||
// Name is the node's MagicDNS base name (for normal non-shared-in
|
||||
// nodes), FQDN (without trailing dot, for shared-in nodes), or
|
||||
// Hostname (if no MagicDNS). Hostname is only included in the
|
||||
// return value if it varies from Name and forOwner is provided true.
|
||||
//
|
||||
// DisplayName is only valid if InitDisplayNames has been called.
|
||||
func (n *Node) DisplayName(forOwner bool) string {
|
||||
if forOwner {
|
||||
return n.ComputedNameWithHost
|
||||
}
|
||||
return n.ComputedName
|
||||
}
|
||||
|
||||
// DisplayName returns the decomposed user-facing name for a node.
|
||||
//
|
||||
// Parameter forOwner specifies whether the name is requested by
|
||||
// the owner of the node. When forOwner is false, hostIfDifferent
|
||||
// is always returned empty.
|
||||
//
|
||||
// Return value name is the node's primary name, populated with the
|
||||
// node's MagicDNS base name (for normal non-shared-in nodes), FQDN
|
||||
// (without trailing dot, for shared-in nodes), or Hostname (if no
|
||||
// MagicDNS).
|
||||
//
|
||||
// Return value hostIfDifferent, when non-empty, is the node's
|
||||
// hostname. hostIfDifferent is only populated when the hostname
|
||||
// varies from name and forOwner is provided as true.
|
||||
//
|
||||
// DisplayNames is only valid if InitDisplayNames has been called.
|
||||
func (n *Node) DisplayNames(forOwner bool) (name, hostIfDifferent string) {
|
||||
if forOwner {
|
||||
return n.ComputedName, n.computedHostIfDifferent
|
||||
}
|
||||
return n.ComputedName, ""
|
||||
}
|
||||
|
||||
// InitDisplayNames computes and populates n's display name
|
||||
// fields: n.ComputedName, n.computedHostIfDifferent, and
|
||||
// n.ComputedNameWithHost.
|
||||
func (n *Node) InitDisplayNames(networkMagicDNSSuffix string) {
|
||||
dnsName := n.Name
|
||||
if dnsName != "" {
|
||||
dnsName = strings.TrimRight(dnsName, ".")
|
||||
if i := strings.Index(dnsName, "."); i != -1 && dnsname.HasSuffix(dnsName, networkMagicDNSSuffix) {
|
||||
dnsName = dnsName[:i]
|
||||
}
|
||||
}
|
||||
|
||||
name := dnsName
|
||||
hostIfDifferent := n.Hostinfo.Hostname
|
||||
|
||||
if strings.EqualFold(name, hostIfDifferent) {
|
||||
hostIfDifferent = ""
|
||||
}
|
||||
if name == "" {
|
||||
if hostIfDifferent != "" {
|
||||
name = hostIfDifferent
|
||||
hostIfDifferent = ""
|
||||
} else {
|
||||
name = n.Key.String()
|
||||
}
|
||||
}
|
||||
|
||||
var nameWithHost string
|
||||
if hostIfDifferent != "" {
|
||||
nameWithHost = fmt.Sprintf("%s (%s)", name, hostIfDifferent)
|
||||
} else {
|
||||
nameWithHost = name
|
||||
}
|
||||
|
||||
n.ComputedName = name
|
||||
n.computedHostIfDifferent = hostIfDifferent
|
||||
n.ComputedNameWithHost = nameWithHost
|
||||
}
|
||||
|
||||
type MachineStatus int
|
||||
@@ -531,6 +634,8 @@ type MapRequest struct {
|
||||
// Current DebugFlags values are:
|
||||
// * "warn-ip-forwarding-off": client is trying to be a subnet
|
||||
// router but their IP forwarding is broken.
|
||||
// * "v6-overlay": IPv6 development flag to have control send
|
||||
// v6 node addrs
|
||||
// * "minimize-netmap": have control minimize the netmap, removing
|
||||
// peers that are unreachable per ACLS.
|
||||
DebugFlags []string `json:",omitempty"`
|
||||
@@ -636,6 +741,11 @@ type MapResponse struct {
|
||||
// PeersRemoved are the NodeIDs that are no longer in the peer list.
|
||||
PeersRemoved []NodeID `json:",omitempty"`
|
||||
|
||||
// PeerSeenChange contains information on how to update peers' LastSeen
|
||||
// times. If the value is false, the peer is gone. If the value is true,
|
||||
// the LastSeen time is now. Absent means unchanged.
|
||||
PeerSeenChange map[NodeID]bool `json:",omitempty"`
|
||||
|
||||
// DNS is the same as DNSConfig.Nameservers.
|
||||
//
|
||||
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
|
||||
@@ -779,6 +889,7 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
}
|
||||
return n != nil && n2 != nil &&
|
||||
n.ID == n2.ID &&
|
||||
n.StableID == n2.StableID &&
|
||||
n.Name == n2.Name &&
|
||||
n.User == n2.User &&
|
||||
n.Sharer == n2.Sharer &&
|
||||
@@ -793,7 +904,10 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
n.Hostinfo.Equal(&n2.Hostinfo) &&
|
||||
n.Created.Equal(n2.Created) &&
|
||||
eqTimePtr(n.LastSeen, n2.LastSeen) &&
|
||||
n.MachineAuthorized == n2.MachineAuthorized
|
||||
n.MachineAuthorized == n2.MachineAuthorized &&
|
||||
n.ComputedName == n2.ComputedName &&
|
||||
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
|
||||
n.ComputedNameWithHost == n2.ComputedNameWithHost
|
||||
}
|
||||
|
||||
func eqStrings(a, b []string) bool {
|
||||
@@ -823,3 +937,9 @@ func eqCIDRs(a, b []netaddr.IPPrefix) bool {
|
||||
func eqTimePtr(a, b *time.Time) bool {
|
||||
return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
|
||||
}
|
||||
|
||||
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
|
||||
type WhoIsResponse struct {
|
||||
Node *Node
|
||||
UserProfile *UserProfile
|
||||
}
|
||||
|
||||
@@ -61,23 +61,27 @@ func (src *Node) Clone() *Node {
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
var _NodeNeedsRegeneration = Node(struct {
|
||||
ID NodeID
|
||||
Name string
|
||||
User UserID
|
||||
Sharer UserID
|
||||
Key NodeKey
|
||||
KeyExpiry time.Time
|
||||
Machine MachineKey
|
||||
DiscoKey DiscoKey
|
||||
Addresses []netaddr.IPPrefix
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints []string
|
||||
DERP string
|
||||
Hostinfo Hostinfo
|
||||
Created time.Time
|
||||
LastSeen *time.Time
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
ID NodeID
|
||||
StableID StableNodeID
|
||||
Name string
|
||||
User UserID
|
||||
Sharer UserID
|
||||
Key NodeKey
|
||||
KeyExpiry time.Time
|
||||
Machine MachineKey
|
||||
DiscoKey DiscoKey
|
||||
Addresses []netaddr.IPPrefix
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints []string
|
||||
DERP string
|
||||
Hostinfo Hostinfo
|
||||
Created time.Time
|
||||
LastSeen *time.Time
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
ComputedNameWithHost string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Hostinfo.
|
||||
|
||||
@@ -189,10 +189,11 @@ func TestHostinfoEqual(t *testing.T) {
|
||||
|
||||
func TestNodeEqual(t *testing.T) {
|
||||
nodeHandles := []string{
|
||||
"ID", "Name", "User", "Sharer",
|
||||
"ID", "StableID", "Name", "User", "Sharer",
|
||||
"Key", "KeyExpiry", "Machine", "DiscoKey",
|
||||
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
||||
"Created", "LastSeen", "KeepAlive", "MachineAuthorized",
|
||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
|
||||
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
@@ -229,6 +230,31 @@ func TestNodeEqual(t *testing.T) {
|
||||
&Node{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{},
|
||||
&Node{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{ID: 1},
|
||||
&Node{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{ID: 1},
|
||||
&Node{ID: 1},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{StableID: "node-abcd"},
|
||||
&Node{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{StableID: "node-abcd"},
|
||||
&Node{StableID: "node-abcd"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{User: 0},
|
||||
&Node{User: 1},
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
wgconn "github.com/tailscale/wireguard-go/conn"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
@@ -758,7 +759,8 @@ func (c *conn) canRead() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closed {
|
||||
return errors.New("closed network connection") // sadface: magic string used by other; don't change
|
||||
// TODO: when we switch to Go 1.16, replace this with net.ErrClosed
|
||||
return wgconn.NetErrClosed
|
||||
}
|
||||
if !c.readDeadline.IsZero() && c.readDeadline.Before(time.Now()) {
|
||||
return errors.New("read deadline exceeded")
|
||||
|
||||
@@ -14,64 +14,30 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type ResourceCheck struct {
|
||||
startNumRoutines int
|
||||
startDump string
|
||||
}
|
||||
|
||||
func NewResourceCheck() *ResourceCheck {
|
||||
// NOTE(apenwarr): I'd rather not pre-generate a goroutine dump here.
|
||||
// However, it turns out to be tricky to debug when eg. the initial
|
||||
// goroutine count > the ending goroutine count, because of course
|
||||
// the missing ones are not in the final dump. Also, we have to
|
||||
// render the profile as a string right away, because the
|
||||
// pprof.Profile object doesn't stay stable over time. Every time
|
||||
// you render the string, you might get a different answer.
|
||||
r := &ResourceCheck{}
|
||||
r.startNumRoutines, r.startDump = goroutineDump()
|
||||
return r
|
||||
}
|
||||
|
||||
func goroutineDump() (int, string) {
|
||||
p := pprof.Lookup("goroutine")
|
||||
b := &bytes.Buffer{}
|
||||
p.WriteTo(b, 1)
|
||||
return p.Count(), b.String()
|
||||
}
|
||||
|
||||
func (r *ResourceCheck) Assert(t testing.TB) {
|
||||
if t.Failed() {
|
||||
// Something else went wrong.
|
||||
// Assume that that is responsible for the leak
|
||||
// and don't pile on a bunch of extra of output.
|
||||
return
|
||||
}
|
||||
t.Helper()
|
||||
want := r.startNumRoutines
|
||||
|
||||
// Some goroutines might be still exiting, so give them a chance
|
||||
got := runtime.NumGoroutine()
|
||||
if want != got {
|
||||
_, dump := goroutineDump()
|
||||
func ResourceCheck(tb testing.TB) {
|
||||
startN, startStacks := goroutines()
|
||||
tb.Cleanup(func() {
|
||||
if tb.Failed() {
|
||||
// Something else went wrong.
|
||||
return
|
||||
}
|
||||
tb.Helper()
|
||||
// Goroutines might be still exiting.
|
||||
for i := 0; i < 100; i++ {
|
||||
got = runtime.NumGoroutine()
|
||||
if want == got {
|
||||
break
|
||||
if runtime.NumGoroutine() <= startN {
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
// If the count is *still* wrong, that's a failure.
|
||||
if want != got {
|
||||
t.Logf("goroutine diff:\n%v\n", cmp.Diff(r.startDump, dump))
|
||||
t.Logf("goroutine count: expected %d, got %d\n", want, got)
|
||||
// Don't fail if there are *fewer* goroutines than
|
||||
// expected. That just might be some leftover ones
|
||||
// from the previous test, which are pretty hard to
|
||||
// eliminate.
|
||||
if want < got {
|
||||
t.Fatalf("ResourceCheck: goroutine count: expected %d, got %d\n", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
endN, endStacks := goroutines()
|
||||
tb.Logf("goroutine diff:\n%v\n", cmp.Diff(startStacks, endStacks))
|
||||
tb.Fatalf("goroutine count: expected %d, got %d\n", startN, endN)
|
||||
})
|
||||
}
|
||||
|
||||
func goroutines() (int, []byte) {
|
||||
p := pprof.Lookup("goroutine")
|
||||
b := new(bytes.Buffer)
|
||||
p.WriteTo(b, 1)
|
||||
return p.Count(), b.Bytes()
|
||||
}
|
||||
|
||||
44
tstime/jitter.go
Normal file
44
tstime/jitter.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tstime
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// crandSource is a rand.Source64 that gets its numbers from
|
||||
// crypto/rand.Reader.
|
||||
type crandSource struct{ sync.Mutex }
|
||||
|
||||
var _ rand.Source64 = (*crandSource)(nil)
|
||||
|
||||
func (s *crandSource) Int63() int64 { return int64(s.Uint64() >> 1) }
|
||||
|
||||
func (s *crandSource) Uint64() uint64 {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
var buf [8]byte
|
||||
crand.Read(buf[:])
|
||||
return binary.BigEndian.Uint64(buf[:])
|
||||
}
|
||||
|
||||
func (*crandSource) Seed(seed int64) {} // nope
|
||||
|
||||
var durRand = rand.New(new(crandSource))
|
||||
|
||||
// RandomDurationBetween returns a random duration in range [min,max).
|
||||
// If panics if max < min.
|
||||
func RandomDurationBetween(min, max time.Duration) time.Duration {
|
||||
diff := max - min
|
||||
if diff == 0 {
|
||||
return min
|
||||
}
|
||||
ns := durRand.Int63n(int64(diff))
|
||||
return min + time.Duration(ns)
|
||||
}
|
||||
23
tstime/jitter_test.go
Normal file
23
tstime/jitter_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tstime
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRandomDurationBetween(t *testing.T) {
|
||||
if got := RandomDurationBetween(1, 1); got != 1 {
|
||||
t.Errorf("between 1 and 1 = %v; want 1", int64(got))
|
||||
}
|
||||
const min = 1 * time.Second
|
||||
const max = 10 * time.Second
|
||||
for i := 0; i < 500; i++ {
|
||||
if got := RandomDurationBetween(min, max); got < min || got >= max {
|
||||
t.Fatalf("%v (%d) out of range", got, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -41,6 +42,7 @@ func NewMux(debugHandler http.Handler) *http.ServeMux {
|
||||
|
||||
func registerCommonDebug(mux *http.ServeMux) {
|
||||
expvar.Publish("counter_uptime_sec", expvar.Func(func() interface{} { return int64(Uptime().Seconds()) }))
|
||||
expvar.Publish("gauge_goroutines", expvar.Func(func() interface{} { return runtime.NumGoroutine() }))
|
||||
mux.Handle("/debug/pprof/", Protected(http.DefaultServeMux)) // to net/http/pprof
|
||||
mux.Handle("/debug/vars", Protected(http.DefaultServeMux)) // to expvar
|
||||
mux.Handle("/debug/varz", Protected(http.HandlerFunc(VarzHandler)))
|
||||
@@ -81,8 +83,11 @@ func AllowDebugAccess(r *http.Request) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ip := net.ParseIP(ipStr)
|
||||
if interfaces.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("TS_ALLOW_DEBUG_IP") {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("TS_ALLOW_DEBUG_IP") {
|
||||
return true
|
||||
}
|
||||
if r.Method == "GET" {
|
||||
|
||||
@@ -64,9 +64,9 @@ type limitData struct {
|
||||
|
||||
var disableRateLimit = os.Getenv("TS_DEBUG_LOG_RATE") == "all"
|
||||
|
||||
// rateFreePrefix are format string prefixes that are exempt from rate limiting.
|
||||
// rateFree are format string substrings that are exempt from rate limiting.
|
||||
// Things should not be added to this unless they're already limited otherwise.
|
||||
var rateFreePrefix = []string{
|
||||
var rateFree = []string{
|
||||
"magicsock: disco: ",
|
||||
"magicsock: CreateEndpoint:",
|
||||
}
|
||||
@@ -93,8 +93,8 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
|
||||
)
|
||||
|
||||
judge := func(format string) verdict {
|
||||
for _, pfx := range rateFreePrefix {
|
||||
if strings.HasPrefix(format, pfx) {
|
||||
for _, sub := range rateFree {
|
||||
if strings.Contains(format, sub) {
|
||||
return allow
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
|
||||
logf(format, args...)
|
||||
case warn:
|
||||
// For the warning, log the specific format string
|
||||
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, fmt.Sprintf(format, args...))
|
||||
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, strings.TrimSpace(fmt.Sprintf(format, args...)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,26 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
// Package netmap contains the netmap.NetworkMap type.
|
||||
package netmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
type NetworkMap struct {
|
||||
// Core networking
|
||||
|
||||
SelfNode *tailcfg.Node
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgkey.Private
|
||||
Expiry time.Time
|
||||
@@ -63,27 +60,16 @@ type NetworkMap struct {
|
||||
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
||||
}
|
||||
|
||||
// MagicDNSSuffix returns the domain's MagicDNS suffix, or empty if none.
|
||||
// If non-empty, it will neither start nor end with a period.
|
||||
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if
|
||||
// MagicDNS isn't necessarily in use).
|
||||
//
|
||||
// It will neither start nor end with a period.
|
||||
func (nm *NetworkMap) MagicDNSSuffix() string {
|
||||
searchPathUsedAsDNSSuffix := func(suffix string) bool {
|
||||
if dnsname.HasSuffix(nm.Name, suffix) {
|
||||
return true
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if dnsname.HasSuffix(p.Name, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
name := strings.Trim(nm.Name, ".")
|
||||
if i := strings.Index(name, "."); i != -1 {
|
||||
name = name[i+1:]
|
||||
}
|
||||
|
||||
for _, d := range nm.DNS.Domains {
|
||||
if searchPathUsedAsDNSSuffix(d) {
|
||||
return strings.Trim(d, ".")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return name
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) String() string {
|
||||
@@ -260,110 +246,8 @@ type WGConfigFlags int
|
||||
const (
|
||||
AllowSingleHosts WGConfigFlags = 1 << iota
|
||||
AllowSubnetRoutes
|
||||
AllowDefaultRoute
|
||||
)
|
||||
|
||||
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
|
||||
// and is then the sole wireguard endpoint for peers with a non-zero discovery key.
|
||||
// This form is then recognize by magicsock's CreateEndpoint.
|
||||
const EndpointDiscoSuffix = ".disco.tailscale:12345"
|
||||
|
||||
// WGCfg returns the NetworkMaps's Wireguard configuration.
|
||||
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
|
||||
cfg := &wgcfg.Config{
|
||||
Name: "tailscale",
|
||||
PrivateKey: wgcfg.PrivateKey(nm.PrivateKey),
|
||||
Addresses: nm.Addresses,
|
||||
ListenPort: nm.LocalPort,
|
||||
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
|
||||
}
|
||||
|
||||
for _, peer := range nm.Peers {
|
||||
if Debug.OnlyDisco && peer.DiscoKey.IsZero() {
|
||||
continue
|
||||
}
|
||||
if (flags&AllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
|
||||
logf("wgcfg: %v skipping a single-host peer.", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
cfg.Peers = append(cfg.Peers, wgcfg.Peer{
|
||||
PublicKey: wgcfg.Key(peer.Key),
|
||||
})
|
||||
cpeer := &cfg.Peers[len(cfg.Peers)-1]
|
||||
if peer.KeepAlive {
|
||||
cpeer.PersistentKeepalive = 25 // seconds
|
||||
}
|
||||
|
||||
if !peer.DiscoKey.IsZero() {
|
||||
if err := appendEndpoint(cpeer, fmt.Sprintf("%x%s", peer.DiscoKey[:], EndpointDiscoSuffix)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cpeer.Endpoints = fmt.Sprintf("%x.disco.tailscale:12345", peer.DiscoKey[:])
|
||||
} else {
|
||||
if err := appendEndpoint(cpeer, peer.DERP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ep := range peer.Endpoints {
|
||||
if err := appendEndpoint(cpeer, ep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Bits == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// cidrIsSubnet reports whether cidr is a non-default-route subnet
|
||||
// exported by node that is not one of its own self addresses.
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {
|
||||
if cidr.Bits == 0 {
|
||||
return false
|
||||
}
|
||||
if !cidr.IsSingleIP() {
|
||||
return true
|
||||
}
|
||||
for _, selfCIDR := range node.Addresses {
|
||||
if cidr == selfCIDR {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func appendEndpoint(peer *wgcfg.Peer, epStr string) error {
|
||||
if epStr == "" {
|
||||
return nil
|
||||
}
|
||||
_, port, err := net.SplitHostPort(epStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
_, err = strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port in endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
if peer.Endpoints != "" {
|
||||
peer.Endpoints += ","
|
||||
}
|
||||
peer.Endpoints += epStr
|
||||
return nil
|
||||
}
|
||||
|
||||
// eqStringsIgnoreNil reports whether a and b have the same length and
|
||||
// contents, but ignore whether a or b are nil.
|
||||
func eqStringsIgnoreNil(a, b []string) bool {
|
||||
@@ -2,11 +2,10 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
package netmap
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -283,15 +282,3 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
73
types/persist/persist.go
Normal file
73
types/persist/persist.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package persist contains the Persist type.
|
||||
package persist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=persist_clone.go
|
||||
|
||||
// Persist is the JSON type stored on disk on nodes to remember their
|
||||
// settings between runs.
|
||||
type Persist struct {
|
||||
_ structs.Incomparable
|
||||
|
||||
// LegacyFrontendPrivateMachineKey is here temporarily
|
||||
// (starting 2020-09-28) during migration of Windows users'
|
||||
// machine keys from frontend storage to the backend. On the
|
||||
// first LocalBackend.Start call, the backend will initialize
|
||||
// the real (backend-owned) machine key from the frontend's
|
||||
// provided value (if non-zero), picking a new random one if
|
||||
// needed. This field should be considered read-only from GUI
|
||||
// frontends. The real value should not be written back in
|
||||
// this field, lest the frontend persist it to disk.
|
||||
LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"`
|
||||
|
||||
PrivateNodeKey wgkey.Private
|
||||
OldPrivateNodeKey wgkey.Private // needed to request key rotation
|
||||
Provider string
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func (p *Persist) Equals(p2 *Persist) bool {
|
||||
if p == nil && p2 == nil {
|
||||
return true
|
||||
}
|
||||
if p == nil || p2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
|
||||
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
|
||||
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
||||
p.Provider == p2.Provider &&
|
||||
p.LoginName == p2.LoginName
|
||||
}
|
||||
|
||||
func (p *Persist) Pretty() string {
|
||||
var mk, ok, nk wgkey.Key
|
||||
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
mk = p.LegacyFrontendPrivateMachineKey.Public()
|
||||
}
|
||||
if !p.OldPrivateNodeKey.IsZero() {
|
||||
ok = p.OldPrivateNodeKey.Public()
|
||||
}
|
||||
if !p.PrivateNodeKey.IsZero() {
|
||||
nk = p.PrivateNodeKey.Public()
|
||||
}
|
||||
ss := func(k wgkey.Key) string {
|
||||
if k.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return k.ShortString()
|
||||
}
|
||||
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
|
||||
ss(mk), ss(ok), ss(nk), p.LoginName)
|
||||
}
|
||||
34
types/persist/persist_clone.go
Normal file
34
types/persist/persist_clone.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Persist; DO NOT EDIT.
|
||||
|
||||
package persist
|
||||
|
||||
import (
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Persist.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Persist) Clone() *Persist {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Persist)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Persist
|
||||
var _PersistNeedsRegeneration = Persist(struct {
|
||||
_ structs.Incomparable
|
||||
LegacyFrontendPrivateMachineKey wgkey.Private
|
||||
PrivateNodeKey wgkey.Private
|
||||
OldPrivateNodeKey wgkey.Private
|
||||
Provider string
|
||||
LoginName string
|
||||
}{})
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
package persist
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -11,6 +11,15 @@ import (
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if name := t.Field(i).Name; name != "_" {
|
||||
fields = append(fields, name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestPersistEqual(t *testing.T) {
|
||||
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
|
||||
if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) {
|
||||
30
types/preftype/netfiltermode.go
Normal file
30
types/preftype/netfiltermode.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package preftype is a leaf package containing types for various
|
||||
// preferences.
|
||||
package preftype
|
||||
|
||||
// NetfilterMode is the firewall management mode to use when
|
||||
// programming the Linux network stack.
|
||||
type NetfilterMode int
|
||||
|
||||
const (
|
||||
NetfilterOff NetfilterMode = iota // remove all tailscale netfilter state
|
||||
NetfilterNoDivert // manage tailscale chains, but don't call them
|
||||
NetfilterOn // manage tailscale chains and call them from main chains
|
||||
)
|
||||
|
||||
func (m NetfilterMode) String() string {
|
||||
switch m {
|
||||
case NetfilterOff:
|
||||
return "off"
|
||||
case NetfilterNoDivert:
|
||||
return "nodivert"
|
||||
case NetfilterOn:
|
||||
return "on"
|
||||
default:
|
||||
return "???"
|
||||
}
|
||||
}
|
||||
13
util/cibuild/cibuild.go
Normal file
13
util/cibuild/cibuild.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package cibuild reports runtime CI information.
|
||||
package cibuild
|
||||
|
||||
import "os"
|
||||
|
||||
// On reports whether the current binary is executing on a CI system.
|
||||
func On() bool {
|
||||
return os.Getenv("GITHUB_ACTIONS") != ""
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !darwin !arm64
|
||||
// +build go1.16,!ios !go1.16,!darwin !go1.16,!arm64
|
||||
|
||||
package version
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,arm64
|
||||
// +build go1.16,ios !go1.16,darwin,arm64
|
||||
|
||||
package version
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ package version
|
||||
// Long is a full version number for this build, of the form
|
||||
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
|
||||
// provided.
|
||||
const Long = "date.20210104"
|
||||
const Long = "date.20210211"
|
||||
|
||||
// Short is a short version number for this build, of the form
|
||||
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
|
||||
|
||||
@@ -132,8 +132,16 @@ func NewAllowNone(logf logger.Logf) *Filter {
|
||||
return New(nil, nil, nil, logf)
|
||||
}
|
||||
|
||||
func NewShieldsUpFilter(logf logger.Logf) *Filter {
|
||||
f := New(nil, nil, nil, logf)
|
||||
// NewShieldsUpFilter returns a packet filter that rejects incoming connections.
|
||||
//
|
||||
// If shareStateWith is non-nil, the returned filter shares state with the previous one,
|
||||
// as long as the previous one was also a shields up filter.
|
||||
func NewShieldsUpFilter(localNets []netaddr.IPPrefix, shareStateWith *Filter, logf logger.Logf) *Filter {
|
||||
// Don't permit sharing state with a prior filter that wasn't a shields-up filter.
|
||||
if shareStateWith != nil && !shareStateWith.shieldsUp {
|
||||
shareStateWith = nil
|
||||
}
|
||||
f := New(nil, localNets, shareStateWith, logf)
|
||||
f.shieldsUp = true
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner --type=Match --output=match_clone.go
|
||||
|
||||
// PortRange is a range of TCP and UDP ports.
|
||||
type PortRange struct {
|
||||
First, Last uint16 // inclusive
|
||||
|
||||
31
wgengine/filter/match_clone.go
Normal file
31
wgengine/filter/match_clone.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Match; DO NOT EDIT.
|
||||
|
||||
package filter
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Match.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Match) Clone() *Match {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Match)
|
||||
*dst = *src
|
||||
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
|
||||
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Match
|
||||
var _MatchNeedsRegeneration = Match(struct {
|
||||
Dsts []NetPortRange
|
||||
Srcs []netaddr.IPPrefix
|
||||
}{})
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
"github.com/tailscale/wireguard-go/conn"
|
||||
"github.com/tailscale/wireguard-go/tai64n"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/poly1305"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -53,7 +53,6 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End
|
||||
return nil, fmt.Errorf("bogus address %q", ep)
|
||||
}
|
||||
a.ipPorts = append(a.ipPorts, ipp)
|
||||
a.addrs = append(a.addrs, *ipp.UDPAddr())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,14 +83,14 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (c *Conn) findLegacyEndpointLocked(ipp netaddr.IPPort, addr *net.UDPAddr, packet []byte) conn.Endpoint {
|
||||
func (c *Conn) findLegacyEndpointLocked(ipp netaddr.IPPort, packet []byte) conn.Endpoint {
|
||||
if c.disableLegacy {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pre-disco: look up their addrSet.
|
||||
if as, ok := c.addrsByUDP[ipp]; ok {
|
||||
as.updateDst(addr)
|
||||
as.updateDst(ipp)
|
||||
return as
|
||||
}
|
||||
|
||||
@@ -100,7 +99,7 @@ func (c *Conn) findLegacyEndpointLocked(ipp netaddr.IPPort, addr *net.UDPAddr, p
|
||||
// know. If this is a handshake packet, we can try to identify the
|
||||
// peer in question.
|
||||
if as := c.peerFromPacketLocked(packet); as != nil {
|
||||
as.updateDst(addr)
|
||||
as.updateDst(ipp)
|
||||
return as
|
||||
}
|
||||
|
||||
@@ -268,14 +267,6 @@ func (as *addrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP
|
||||
|
||||
as.lastSend = now
|
||||
|
||||
// Some internal invariant checks.
|
||||
if len(as.addrs) != len(as.ipPorts) {
|
||||
panic(fmt.Sprintf("lena %d != leni %d", len(as.addrs), len(as.ipPorts)))
|
||||
}
|
||||
if n1, n2 := as.roamAddr != nil, as.roamAddrStd != nil; n1 != n2 {
|
||||
panic(fmt.Sprintf("roamnil %v != roamstdnil %v", n1, n2))
|
||||
}
|
||||
|
||||
// Spray logic.
|
||||
//
|
||||
// After exchanging a handshake with a peer, we send some outbound
|
||||
@@ -320,8 +311,8 @@ func (as *addrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP
|
||||
// roamAddr should be special like this.
|
||||
dsts = append(dsts, *as.roamAddr)
|
||||
case as.curAddr != -1:
|
||||
if as.curAddr >= len(as.addrs) {
|
||||
as.Logf("[unexpected] magicsock bug: as.curAddr >= len(as.addrs): %d >= %d", as.curAddr, len(as.addrs))
|
||||
if as.curAddr >= len(as.ipPorts) {
|
||||
as.Logf("[unexpected] magicsock bug: as.curAddr >= len(as.ipPorts): %d >= %d", as.curAddr, len(as.ipPorts))
|
||||
break
|
||||
}
|
||||
// No roaming addr, but we've seen packets from a known peer
|
||||
@@ -352,15 +343,14 @@ func (as *addrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP
|
||||
type addrSet struct {
|
||||
publicKey key.Public // peer public key used for DERP communication
|
||||
|
||||
// addrs is an ordered priority list provided by wgengine,
|
||||
// ipPorts is an ordered priority list provided by wgengine,
|
||||
// sorted from expensive+slow+reliable at the begnining to
|
||||
// fast+cheap at the end. More concretely, it's typically:
|
||||
//
|
||||
// [DERP fakeip:node, Global IP:port, LAN ip:port]
|
||||
//
|
||||
// But there could be multiple or none of each.
|
||||
addrs []net.UDPAddr
|
||||
ipPorts []netaddr.IPPort // same as addrs, in different form
|
||||
ipPorts []netaddr.IPPort
|
||||
|
||||
// clock, if non-nil, is used in tests instead of time.Now.
|
||||
clock func() time.Time
|
||||
@@ -376,8 +366,7 @@ type addrSet struct {
|
||||
// this should hopefully never be used (or at least used
|
||||
// rarely) in the case that all the components of Tailscale
|
||||
// are correctly learning/sharing the network map details.
|
||||
roamAddr *netaddr.IPPort
|
||||
roamAddrStd *net.UDPAddr
|
||||
roamAddr *netaddr.IPPort
|
||||
|
||||
// curAddr is an index into addrs of the highest-priority
|
||||
// address a valid packet has been received from so far.
|
||||
@@ -400,9 +389,9 @@ type addrSet struct {
|
||||
|
||||
// derpID returns this addrSet's home DERP node, or 0 if none is found.
|
||||
func (as *addrSet) derpID() int {
|
||||
for _, ua := range as.addrs {
|
||||
if ua.IP.Equal(derpMagicIP) {
|
||||
return ua.Port
|
||||
for _, ua := range as.ipPorts {
|
||||
if ua.IP == derpMagicIPAddr {
|
||||
return int(ua.Port)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@@ -424,7 +413,7 @@ func (a *addrSet) dst() netaddr.IPPort {
|
||||
if a.roamAddr != nil {
|
||||
return *a.roamAddr
|
||||
}
|
||||
if len(a.addrs) == 0 {
|
||||
if len(a.ipPorts) == 0 {
|
||||
return noAddr
|
||||
}
|
||||
i := a.curAddr
|
||||
@@ -438,8 +427,17 @@ func (a *addrSet) DstToBytes() []byte {
|
||||
return packIPPort(a.dst())
|
||||
}
|
||||
func (a *addrSet) DstToString() string {
|
||||
dst := a.dst()
|
||||
return dst.String()
|
||||
var addrs []string
|
||||
for _, addr := range a.ipPorts {
|
||||
addrs = append(addrs, addr.String())
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.roamAddr != nil {
|
||||
addrs = append(addrs, a.roamAddr.String())
|
||||
}
|
||||
return strings.Join(addrs, ",")
|
||||
}
|
||||
func (a *addrSet) DstIP() net.IP {
|
||||
return a.dst().IP.IPAddr().IP // TODO: add netaddr accessor to cut an alloc here?
|
||||
@@ -450,8 +448,8 @@ func (a *addrSet) ClearSrc() {}
|
||||
|
||||
// updateDst records receipt of a packet from new. This is used to
|
||||
// potentially update the transmit address used for this addrSet.
|
||||
func (a *addrSet) updateDst(new *net.UDPAddr) error {
|
||||
if new.IP.Equal(derpMagicIP) {
|
||||
func (a *addrSet) updateDst(new netaddr.IPPort) error {
|
||||
if new.IP == derpMagicIPAddr {
|
||||
// Never consider DERP addresses as a viable candidate for
|
||||
// either curAddr or roamAddr. It's only ever a last resort
|
||||
// choice, never a preferred choice.
|
||||
@@ -462,25 +460,20 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.roamAddrStd != nil && equalUDPAddr(new, a.roamAddrStd) {
|
||||
if a.roamAddr != nil && new == *a.roamAddr {
|
||||
// Packet from the current roaming address, no logging.
|
||||
// This is a hot path for established connections.
|
||||
return nil
|
||||
}
|
||||
if a.roamAddr == nil && a.curAddr >= 0 && equalUDPAddr(new, &a.addrs[a.curAddr]) {
|
||||
if a.roamAddr == nil && a.curAddr >= 0 && new == a.ipPorts[a.curAddr] {
|
||||
// Packet from current-priority address, no logging.
|
||||
// This is a hot path for established connections.
|
||||
return nil
|
||||
}
|
||||
|
||||
newa, ok := netaddr.FromStdAddr(new.IP, new.Port, new.Zone)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
index := -1
|
||||
for i := range a.addrs {
|
||||
if equalUDPAddr(new, &a.addrs[i]) {
|
||||
for i := range a.ipPorts {
|
||||
if new == a.ipPorts[i] {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
@@ -490,7 +483,7 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error {
|
||||
pk := publicKey.ShortString()
|
||||
old := "<none>"
|
||||
if a.curAddr >= 0 {
|
||||
old = a.addrs[a.curAddr].String()
|
||||
old = a.ipPorts[a.curAddr].String()
|
||||
}
|
||||
|
||||
switch {
|
||||
@@ -500,18 +493,16 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error {
|
||||
} else {
|
||||
a.Logf("magicsock: rx %s from roaming address %s, replaces roaming address %s", pk, new, a.roamAddr)
|
||||
}
|
||||
a.roamAddr = &newa
|
||||
a.roamAddrStd = new
|
||||
a.roamAddr = &new
|
||||
|
||||
case a.roamAddr != nil:
|
||||
a.Logf("magicsock: rx %s from known %s (%d), replaces roaming address %s", pk, new, index, a.roamAddr)
|
||||
a.roamAddr = nil
|
||||
a.roamAddrStd = nil
|
||||
a.curAddr = index
|
||||
a.loggedLogPriMask = 0
|
||||
|
||||
case a.curAddr == -1:
|
||||
a.Logf("magicsock: rx %s from %s (%d/%d), set as new priority", pk, new, index, len(a.addrs))
|
||||
a.Logf("magicsock: rx %s from %s (%d/%d), set as new priority", pk, new, index, len(a.ipPorts))
|
||||
a.curAddr = index
|
||||
a.loggedLogPriMask = 0
|
||||
|
||||
@@ -522,7 +513,7 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error {
|
||||
}
|
||||
|
||||
default: // index > a.curAddr
|
||||
a.Logf("magicsock: rx %s from %s (%d/%d), replaces old priority %s", pk, new, index, len(a.addrs), old)
|
||||
a.Logf("magicsock: rx %s from %s (%d/%d), replaces old priority %s", pk, new, index, len(a.ipPorts), old)
|
||||
a.curAddr = index
|
||||
a.loggedLogPriMask = 0
|
||||
}
|
||||
@@ -530,10 +521,6 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func equalUDPAddr(x, y *net.UDPAddr) bool {
|
||||
return x.Port == y.Port && x.IP.Equal(y.IP)
|
||||
}
|
||||
|
||||
func (a *addrSet) String() string {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
@@ -542,9 +529,9 @@ func (a *addrSet) String() string {
|
||||
buf.WriteByte('[')
|
||||
if a.roamAddr != nil {
|
||||
buf.WriteString("roam:")
|
||||
sbPrintAddr(buf, *a.roamAddrStd)
|
||||
sbPrintAddr(buf, *a.roamAddr)
|
||||
}
|
||||
for i, addr := range a.addrs {
|
||||
for i, addr := range a.ipPorts {
|
||||
if i > 0 || a.roamAddr != nil {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
@@ -563,8 +550,8 @@ func (as *addrSet) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
defer as.mu.Unlock()
|
||||
|
||||
ps.LastWrite = as.lastSend
|
||||
for i, ua := range as.addrs {
|
||||
if ua.IP.Equal(derpMagicIP) {
|
||||
for i, ua := range as.ipPorts {
|
||||
if ua.IP == derpMagicIPAddr {
|
||||
continue
|
||||
}
|
||||
uaStr := ua.String()
|
||||
@@ -574,24 +561,10 @@ func (as *addrSet) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
}
|
||||
}
|
||||
if as.roamAddr != nil {
|
||||
ps.CurAddr = udpAddrDebugString(*as.roamAddrStd)
|
||||
ps.CurAddr = ippDebugString(*as.roamAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *addrSet) Addrs() string {
|
||||
var addrs []string
|
||||
for _, addr := range a.addrs {
|
||||
addrs = append(addrs, addr.String())
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.roamAddr != nil {
|
||||
addrs = append(addrs, a.roamAddr.String())
|
||||
}
|
||||
return strings.Join(addrs, ",")
|
||||
}
|
||||
|
||||
// Message types copied from wireguard-go/device/noise-protocol.go
|
||||
const (
|
||||
messageInitiationType = 1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,12 +11,14 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -28,10 +30,8 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun/tuntest"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
@@ -42,10 +42,15 @@ import (
|
||||
"tailscale.com/tstest/natlab"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/cibuild"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/tstun"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wgcfg/nmcfg"
|
||||
"tailscale.com/wgengine/wglog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -127,6 +132,7 @@ type magicStack struct {
|
||||
tun *tuntest.ChannelTUN // TUN device to send/receive packets
|
||||
tsTun *tstun.TUN // wrapped tun that implements filtering and wgengine hooks
|
||||
dev *device.Device // the wireguard-go Device that connects the previous things
|
||||
wgLogger *wglog.Logger // wireguard-go log wrapper
|
||||
}
|
||||
|
||||
// newMagicStack builds and initializes an idle magicsock and
|
||||
@@ -163,8 +169,9 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
|
||||
tsTun := tstun.WrapTUN(logf, tun.TUN())
|
||||
tsTun.SetFilter(filter.NewAllowAllForTest(logf))
|
||||
|
||||
wgLogger := wglog.NewLogger(logf)
|
||||
dev := device.NewDevice(tsTun, &device.DeviceOptions{
|
||||
Logger: wireguardGoLogger(logf),
|
||||
Logger: wgLogger.DeviceLogger,
|
||||
CreateEndpoint: conn.CreateEndpoint,
|
||||
CreateBind: conn.CreateBind,
|
||||
SkipBindUpdate: true,
|
||||
@@ -187,9 +194,15 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
|
||||
tun: tun,
|
||||
tsTun: tsTun,
|
||||
dev: dev,
|
||||
wgLogger: wgLogger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *magicStack) Reconfig(cfg *wgcfg.Config) error {
|
||||
s.wgLogger.SetPeers(cfg.Peers)
|
||||
return wgcfg.ReconfigDevice(s.dev, cfg, s.conn.logf)
|
||||
}
|
||||
|
||||
func (s *magicStack) String() string {
|
||||
pub := s.Public()
|
||||
return pub.ShortString()
|
||||
@@ -242,9 +255,9 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
|
||||
eps = make([][]string, len(ms))
|
||||
)
|
||||
|
||||
buildNetmapLocked := func(myIdx int) *controlclient.NetworkMap {
|
||||
buildNetmapLocked := func(myIdx int) *netmap.NetworkMap {
|
||||
me := ms[myIdx]
|
||||
nm := &controlclient.NetworkMap{
|
||||
nm := &netmap.NetworkMap{
|
||||
PrivateKey: me.privateKey,
|
||||
NodeKey: tailcfg.NodeKey(me.privateKey.Public()),
|
||||
Addresses: []netaddr.IPPrefix{{IP: netaddr.IPv4(1, 0, 0, byte(myIdx+1)), Bits: 32}},
|
||||
@@ -277,20 +290,20 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
|
||||
eps[idx] = newEps
|
||||
|
||||
for i, m := range ms {
|
||||
netmap := buildNetmapLocked(i)
|
||||
m.conn.SetNetworkMap(netmap)
|
||||
peerSet := make(map[key.Public]struct{}, len(netmap.Peers))
|
||||
for _, peer := range netmap.Peers {
|
||||
nm := buildNetmapLocked(i)
|
||||
m.conn.SetNetworkMap(nm)
|
||||
peerSet := make(map[key.Public]struct{}, len(nm.Peers))
|
||||
for _, peer := range nm.Peers {
|
||||
peerSet[key.Public(peer.Key)] = struct{}{}
|
||||
}
|
||||
m.conn.UpdatePeers(peerSet)
|
||||
wg, err := netmap.WGCfg(logf, controlclient.AllowSingleHosts)
|
||||
wg, err := nmcfg.WGCfg(nm, logf, netmap.AllowSingleHosts)
|
||||
if err != nil {
|
||||
// We're too far from the *testing.T to be graceful,
|
||||
// blow up. Shouldn't happen anyway.
|
||||
panic(fmt.Sprintf("failed to construct wgcfg from netmap: %v", err))
|
||||
}
|
||||
if err := m.dev.Reconfig(wg); err != nil {
|
||||
if err := m.Reconfig(wg); err != nil {
|
||||
panic(fmt.Sprintf("device reconfig failed: %v", err))
|
||||
}
|
||||
}
|
||||
@@ -322,8 +335,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
|
||||
|
||||
func TestNewConn(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
epCh := make(chan string, 16)
|
||||
epFunc := func(endpoints []string) {
|
||||
@@ -387,23 +399,9 @@ func pickPort(t testing.TB) uint16 {
|
||||
return uint16(conn.LocalAddr().(*net.UDPAddr).Port)
|
||||
}
|
||||
|
||||
func TestDerpIPConstant(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
|
||||
if DerpMagicIP != derpMagicIP.String() {
|
||||
t.Errorf("str %q != IP %v", DerpMagicIP, derpMagicIP)
|
||||
}
|
||||
if len(derpMagicIP) != 4 {
|
||||
t.Errorf("derpMagicIP is len %d; want 4", len(derpMagicIP))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickDERPFallback(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
c := newConn()
|
||||
c.derpMap = derpmap.Prod()
|
||||
@@ -443,7 +441,7 @@ func TestPickDERPFallback(t *testing.T) {
|
||||
// But move if peers are elsewhere.
|
||||
const otherNode = 789
|
||||
c.addrsByKey = map[key.Public]*addrSet{
|
||||
key.Public{1}: &addrSet{addrs: []net.UDPAddr{{IP: derpMagicIP, Port: otherNode}}},
|
||||
key.Public{1}: &addrSet{ipPorts: []netaddr.IPPort{{IP: derpMagicIPAddr, Port: otherNode}}},
|
||||
}
|
||||
if got := c.pickDERPFallback(); got != otherNode {
|
||||
t.Errorf("didn't join peers: got %v; want %v", got, someNode)
|
||||
@@ -510,8 +508,7 @@ func parseCIDR(t *testing.T, addr string) netaddr.IPPrefix {
|
||||
// -count=10000 to be sure.
|
||||
func TestDeviceStartStop(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
conn, err := NewConn(Options{
|
||||
EndpointsFunc: func(eps []string) {},
|
||||
@@ -526,7 +523,7 @@ func TestDeviceStartStop(t *testing.T) {
|
||||
|
||||
tun := tuntest.NewChannelTUN()
|
||||
dev := device.NewDevice(tun.TUN(), &device.DeviceOptions{
|
||||
Logger: wireguardGoLogger(t.Logf),
|
||||
Logger: wglog.NewLogger(t.Logf).DeviceLogger,
|
||||
CreateEndpoint: conn.CreateEndpoint,
|
||||
CreateBind: conn.CreateBind,
|
||||
SkipBindUpdate: true,
|
||||
@@ -829,8 +826,7 @@ func newPinger(t *testing.T, logf logger.Logf, src, dst *magicStack) (cleanup fu
|
||||
// get exercised.
|
||||
func testActiveDiscovery(t *testing.T, d *devices) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
tlogf, setT := makeNestable(t)
|
||||
setT(t)
|
||||
@@ -891,12 +887,14 @@ func testActiveDiscovery(t *testing.T, d *devices) {
|
||||
|
||||
func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
// This gets reassigned inside every test, so that the connections
|
||||
// all log using the "current" t.Logf function. Sigh.
|
||||
logf, setT := makeNestable(t)
|
||||
nestedLogf, setT := makeNestable(t)
|
||||
|
||||
logf, closeLogf := logger.LogfCloser(nestedLogf)
|
||||
defer closeLogf()
|
||||
|
||||
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
|
||||
defer cleanup()
|
||||
@@ -912,37 +910,63 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
}
|
||||
cfgs := makeConfigs(t, addrs)
|
||||
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
|
||||
if err := m2.Reconfig(&cfgs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// In the normal case, pings succeed immediately.
|
||||
// However, in the case of a handshake race, we need to retry.
|
||||
// With very bad luck, we can need to retry multiple times.
|
||||
allowedRetries := 3
|
||||
if cibuild.On() {
|
||||
// Allow extra retries on small/flaky/loaded CI machines.
|
||||
allowedRetries *= 2
|
||||
}
|
||||
// Retries take 5s each. Add 1s for some processing time.
|
||||
pingTimeout := 5*time.Second*time.Duration(allowedRetries) + time.Second
|
||||
|
||||
// sendWithTimeout sends msg using send, checking that it is received unchanged from in.
|
||||
// It resends once per second until the send succeeds, or pingTimeout time has elapsed.
|
||||
sendWithTimeout := func(msg []byte, in chan []byte, send func()) error {
|
||||
start := time.Now()
|
||||
for time.Since(start) < pingTimeout {
|
||||
send()
|
||||
select {
|
||||
case recv := <-in:
|
||||
if !bytes.Equal(msg, recv) {
|
||||
return errors.New("ping did not transit correctly")
|
||||
}
|
||||
return nil
|
||||
case <-time.After(time.Second):
|
||||
// try again
|
||||
}
|
||||
}
|
||||
return errors.New("ping timed out")
|
||||
}
|
||||
|
||||
ping1 := func(t *testing.T) {
|
||||
msg2to1 := tuntest.Ping(net.ParseIP("1.0.0.1"), net.ParseIP("1.0.0.2"))
|
||||
m2.tun.Outbound <- msg2to1
|
||||
t.Log("ping1 sent")
|
||||
select {
|
||||
case msgRecv := <-m1.tun.Inbound:
|
||||
if !bytes.Equal(msg2to1, msgRecv) {
|
||||
t.Error("ping did not transit correctly")
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Error("ping did not transit")
|
||||
send := func() {
|
||||
m2.tun.Outbound <- msg2to1
|
||||
t.Log("ping1 sent")
|
||||
}
|
||||
in := m1.tun.Inbound
|
||||
if err := sendWithTimeout(msg2to1, in, send); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
ping2 := func(t *testing.T) {
|
||||
msg1to2 := tuntest.Ping(net.ParseIP("1.0.0.2"), net.ParseIP("1.0.0.1"))
|
||||
m1.tun.Outbound <- msg1to2
|
||||
t.Log("ping2 sent")
|
||||
select {
|
||||
case msgRecv := <-m2.tun.Inbound:
|
||||
if !bytes.Equal(msg1to2, msgRecv) {
|
||||
t.Error("return ping did not transit correctly")
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Error("return ping did not transit")
|
||||
send := func() {
|
||||
m1.tun.Outbound <- msg1to2
|
||||
t.Log("ping2 sent")
|
||||
}
|
||||
in := m2.tun.Inbound
|
||||
if err := sendWithTimeout(msg1to2, in, send); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,24 +987,22 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
setT(t)
|
||||
defer setT(outerT)
|
||||
msg1to2 := tuntest.Ping(net.ParseIP("1.0.0.2"), net.ParseIP("1.0.0.1"))
|
||||
if err := m1.tsTun.InjectOutbound(msg1to2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("SendPacket sent")
|
||||
select {
|
||||
case msgRecv := <-m2.tun.Inbound:
|
||||
if !bytes.Equal(msg1to2, msgRecv) {
|
||||
t.Error("return ping did not transit correctly")
|
||||
send := func() {
|
||||
if err := m1.tsTun.InjectOutbound(msg1to2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Error("return ping did not transit")
|
||||
t.Log("SendPacket sent")
|
||||
}
|
||||
in := m2.tun.Inbound
|
||||
if err := sendWithTimeout(msg1to2, in, send); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no-op dev1 reconfig", func(t *testing.T) {
|
||||
setT(t)
|
||||
defer setT(outerT)
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ping1(t)
|
||||
@@ -1035,7 +1057,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
t.Errorf("return ping %d did not transit correctly: %s", i, cmp.Diff(b, msgRecv))
|
||||
}
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
case <-time.After(pingTimeout):
|
||||
if strict {
|
||||
t.Errorf("return ping %d did not transit", i)
|
||||
}
|
||||
@@ -1057,10 +1079,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
ep1 := cfgs[1].Peers[0].Endpoints
|
||||
ep1 = derpEp + "," + ep1
|
||||
cfgs[1].Peers[0].Endpoints = ep1
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
|
||||
if err := m2.Reconfig(&cfgs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1073,10 +1095,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
// Disable real route.
|
||||
cfgs[0].Peers[0].Endpoints = derpEp
|
||||
cfgs[1].Peers[0].Endpoints = derpEp
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
|
||||
if err := m2.Reconfig(&cfgs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond) // TODO remove
|
||||
@@ -1102,10 +1124,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
if ep2 := cfgs[1].Peers[0].Endpoints; len(ep2) != 1 {
|
||||
t.Errorf("unexpected peer endpoints in dev2: %v", ep2)
|
||||
}
|
||||
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
|
||||
if err := m2.Reconfig(&cfgs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Dear future human debugging a test failure here: this test is
|
||||
@@ -1119,7 +1141,11 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
defer setT(outerT)
|
||||
pingSeq(t, 50, 700*time.Millisecond, false)
|
||||
|
||||
ep2 := m2.dev.Config().Peers[0].Endpoints
|
||||
cfg, err := wgcfg.DeviceConfig(m2.dev)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ep2 := cfg.Peers[0].Endpoints
|
||||
if len(ep2) != 2 {
|
||||
t.Error("handshake spray failed to find real route")
|
||||
}
|
||||
@@ -1129,24 +1155,16 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
// TestAddrSet tests addrSet appendDests and updateDst.
|
||||
func TestAddrSet(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
mustIPPortPtr := func(s string) *netaddr.IPPort {
|
||||
t.Helper()
|
||||
ipp, err := netaddr.ParseIPPort(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ipp := netaddr.MustParseIPPort(s)
|
||||
return &ipp
|
||||
}
|
||||
mustUDPAddr := func(s string) *net.UDPAddr {
|
||||
return mustIPPortPtr(s).UDPAddr()
|
||||
}
|
||||
udpAddrs := func(ss ...string) (ret []net.UDPAddr) {
|
||||
ipps := func(ss ...string) (ret []netaddr.IPPort) {
|
||||
t.Helper()
|
||||
for _, s := range ss {
|
||||
ret = append(ret, *mustUDPAddr(s))
|
||||
ret = append(ret, netaddr.MustParseIPPort(s))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -1178,7 +1196,7 @@ func TestAddrSet(t *testing.T) {
|
||||
|
||||
// updateDst, if set, does an UpdateDst call and
|
||||
// b+want are ignored.
|
||||
updateDst *net.UDPAddr
|
||||
updateDst *netaddr.IPPort
|
||||
|
||||
b []byte
|
||||
want string // comma-separated
|
||||
@@ -1192,7 +1210,7 @@ func TestAddrSet(t *testing.T) {
|
||||
{
|
||||
name: "reg_packet_no_curaddr",
|
||||
as: &addrSet{
|
||||
addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
curAddr: -1, // unknown
|
||||
roamAddr: nil,
|
||||
},
|
||||
@@ -1203,7 +1221,7 @@ func TestAddrSet(t *testing.T) {
|
||||
{
|
||||
name: "reg_packet_have_curaddr",
|
||||
as: &addrSet{
|
||||
addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
curAddr: 1, // global IP
|
||||
roamAddr: nil,
|
||||
},
|
||||
@@ -1214,36 +1232,36 @@ func TestAddrSet(t *testing.T) {
|
||||
{
|
||||
name: "reg_packet_have_roamaddr",
|
||||
as: &addrSet{
|
||||
addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
curAddr: 2, // should be ignored
|
||||
roamAddr: mustIPPortPtr("5.6.7.8:123"),
|
||||
},
|
||||
steps: []step{
|
||||
{b: regPacket, want: "5.6.7.8:123"},
|
||||
{updateDst: mustUDPAddr("10.0.0.1:123")}, // no more roaming
|
||||
{updateDst: mustIPPortPtr("10.0.0.1:123")}, // no more roaming
|
||||
{b: regPacket, want: "10.0.0.1:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start_roaming",
|
||||
as: &addrSet{
|
||||
addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
curAddr: 2,
|
||||
},
|
||||
steps: []step{
|
||||
{b: regPacket, want: "10.0.0.1:123"},
|
||||
{updateDst: mustUDPAddr("4.5.6.7:123")},
|
||||
{updateDst: mustIPPortPtr("4.5.6.7:123")},
|
||||
{b: regPacket, want: "4.5.6.7:123"},
|
||||
{updateDst: mustUDPAddr("5.6.7.8:123")},
|
||||
{updateDst: mustIPPortPtr("5.6.7.8:123")},
|
||||
{b: regPacket, want: "5.6.7.8:123"},
|
||||
{updateDst: mustUDPAddr("123.45.67.89:123")}, // end roaming
|
||||
{updateDst: mustIPPortPtr("123.45.67.89:123")}, // end roaming
|
||||
{b: regPacket, want: "123.45.67.89:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "spray_packet",
|
||||
as: &addrSet{
|
||||
addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
curAddr: 2, // should be ignored
|
||||
roamAddr: mustIPPortPtr("5.6.7.8:123"),
|
||||
},
|
||||
@@ -1252,19 +1270,19 @@ func TestAddrSet(t *testing.T) {
|
||||
{advance: 300 * time.Millisecond, b: regPacket, want: "127.3.3.40:1,123.45.67.89:123,10.0.0.1:123,5.6.7.8:123"},
|
||||
{advance: 300 * time.Millisecond, b: regPacket, want: "127.3.3.40:1,123.45.67.89:123,10.0.0.1:123,5.6.7.8:123"},
|
||||
{advance: 3, b: regPacket, want: "5.6.7.8:123"},
|
||||
{advance: 2 * time.Millisecond, updateDst: mustUDPAddr("10.0.0.1:123")},
|
||||
{advance: 2 * time.Millisecond, updateDst: mustIPPortPtr("10.0.0.1:123")},
|
||||
{advance: 3, b: regPacket, want: "10.0.0.1:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "low_pri",
|
||||
as: &addrSet{
|
||||
addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"),
|
||||
curAddr: 2,
|
||||
},
|
||||
steps: []step{
|
||||
{updateDst: mustUDPAddr("123.45.67.89:123")},
|
||||
{updateDst: mustUDPAddr("123.45.67.89:123")},
|
||||
{updateDst: mustIPPortPtr("123.45.67.89:123")},
|
||||
{updateDst: mustIPPortPtr("123.45.67.89:123")},
|
||||
},
|
||||
logCheck: func(t *testing.T, logged []byte) {
|
||||
if n := bytes.Count(logged, []byte(", keeping current ")); n != 1 {
|
||||
@@ -1283,12 +1301,11 @@ func TestAddrSet(t *testing.T) {
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
tt.as.clock = func() time.Time { return faket }
|
||||
initAddrSet(tt.as)
|
||||
for i, st := range tt.steps {
|
||||
faket = faket.Add(st.advance)
|
||||
|
||||
if st.updateDst != nil {
|
||||
if err := tt.as.updateDst(st.updateDst); err != nil {
|
||||
if err := tt.as.updateDst(*st.updateDst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
continue
|
||||
@@ -1305,23 +1322,6 @@ func TestAddrSet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// initAddrSet initializes fields in the provided incomplete addrSet
|
||||
// to satisfying invariants within magicsock.
|
||||
func initAddrSet(as *addrSet) {
|
||||
if as.roamAddr != nil && as.roamAddrStd == nil {
|
||||
as.roamAddrStd = as.roamAddr.UDPAddr()
|
||||
}
|
||||
if len(as.ipPorts) == 0 {
|
||||
for _, ua := range as.addrs {
|
||||
ipp, ok := netaddr.FromStdAddr(ua.IP, ua.Port, ua.Zone)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("bogus UDPAddr %+v", ua))
|
||||
}
|
||||
as.ipPorts = append(as.ipPorts, ipp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoMessage(t *testing.T) {
|
||||
c := newConn()
|
||||
c.logf = t.Logf
|
||||
@@ -1398,62 +1398,235 @@ func Test32bitAlignment(t *testing.T) {
|
||||
atomic.AddInt64(&c.derpRecvCountAtomic, 1)
|
||||
}
|
||||
|
||||
func BenchmarkReceiveFrom(b *testing.B) {
|
||||
port := pickPort(b)
|
||||
// newNonLegacyTestConn returns a new Conn with DisableLegacyNetworking set true.
|
||||
func newNonLegacyTestConn(t testing.TB) *Conn {
|
||||
t.Helper()
|
||||
port := pickPort(t)
|
||||
conn, err := NewConn(Options{
|
||||
Logf: b.Logf,
|
||||
Logf: t.Logf,
|
||||
Port: port,
|
||||
EndpointsFunc: func(eps []string) {
|
||||
b.Logf("endpoints: %q", eps)
|
||||
t.Logf("endpoints: %q", eps)
|
||||
},
|
||||
DisableLegacyNetworking: true,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
// Tests concurrent DERP readers pushing DERP data into ReceiveIPv4
|
||||
// (which should blend all DERP reads into UDP reads).
|
||||
func TestDerpReceiveFromIPv4(t *testing.T) {
|
||||
conn := newNonLegacyTestConn(t)
|
||||
defer conn.Close()
|
||||
|
||||
sendConn, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sendConn.Close()
|
||||
nodeKey, _ := addTestEndpoint(conn, sendConn)
|
||||
|
||||
var sends int = 250e3 // takes about a second
|
||||
if testing.Short() {
|
||||
sends /= 10
|
||||
}
|
||||
senders := runtime.NumCPU()
|
||||
sends -= (sends % senders)
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
t.Logf("doing %v sends over %d senders", sends, senders)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer conn.Close()
|
||||
defer cancel()
|
||||
|
||||
doneCtx, cancelDoneCtx := context.WithCancel(context.Background())
|
||||
cancelDoneCtx()
|
||||
|
||||
for i := 0; i < senders; i++ {
|
||||
wg.Add(1)
|
||||
regionID := i + 1
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < sends/senders; i++ {
|
||||
res := derpReadResult{
|
||||
regionID: regionID,
|
||||
n: 123,
|
||||
src: key.Public(nodeKey),
|
||||
copyBuf: func(dst []byte) int { return 123 },
|
||||
}
|
||||
// First send with the closed context. ~50% of
|
||||
// these should end up going through the
|
||||
// send-a-zero-derpReadResult path, returning
|
||||
// true, in which case we don't want to send again.
|
||||
// We test later that we hit the other path.
|
||||
if conn.sendDerpReadResult(doneCtx, res) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !conn.sendDerpReadResult(ctx, res) {
|
||||
t.Error("unexpected false")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
zeroSendsStart := testCounterZeroDerpReadResultSend.Value()
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
for i := 0; i < sends; i++ {
|
||||
n, ep, err := conn.ReceiveIPv4(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = n
|
||||
_ = ep
|
||||
}
|
||||
|
||||
t.Logf("did %d ReceiveIPv4 calls", sends)
|
||||
|
||||
zeroSends, zeroRecv := testCounterZeroDerpReadResultSend.Value(), testCounterZeroDerpReadResultRecv.Value()
|
||||
if zeroSends != zeroRecv {
|
||||
t.Errorf("did %d zero sends != %d corresponding receives", zeroSends, zeroRecv)
|
||||
}
|
||||
zeroSendDelta := zeroSends - zeroSendsStart
|
||||
if zeroSendDelta == 0 {
|
||||
t.Errorf("didn't see any sends of derpReadResult zero value")
|
||||
}
|
||||
if zeroSendDelta == int64(sends) {
|
||||
t.Errorf("saw %v sends of the derpReadResult zero value which was unexpectedly high (100%% of our %v sends)", zeroSendDelta, sends)
|
||||
}
|
||||
}
|
||||
|
||||
// addTestEndpoint sets conn's network map to a single peer expected
|
||||
// to receive packets from sendConn (or DERP), and returns that peer's
|
||||
// nodekey and discokey.
|
||||
func addTestEndpoint(conn *Conn, sendConn net.PacketConn) (tailcfg.NodeKey, tailcfg.DiscoKey) {
|
||||
// Give conn just enough state that it'll recognize sendConn as a
|
||||
// valid peer and not fall through to the legacy magicsock
|
||||
// codepath.
|
||||
discoKey := tailcfg.DiscoKey{31: 1}
|
||||
conn.SetNetworkMap(&controlclient.NetworkMap{
|
||||
nodeKey := tailcfg.NodeKey{0: 'N', 1: 'K'}
|
||||
conn.SetNetworkMap(&netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
Key: nodeKey,
|
||||
DiscoKey: discoKey,
|
||||
Endpoints: []string{sendConn.LocalAddr().String()},
|
||||
},
|
||||
},
|
||||
})
|
||||
conn.CreateEndpoint([32]byte{1: 1}, "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345")
|
||||
conn.SetPrivateKey(wgkey.Private{0: 1})
|
||||
conn.CreateEndpoint([32]byte(nodeKey), "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345")
|
||||
conn.addValidDiscoPathForTest(discoKey, netaddr.MustParseIPPort(sendConn.LocalAddr().String()))
|
||||
return nodeKey, discoKey
|
||||
}
|
||||
|
||||
func setUpReceiveFrom(tb testing.TB) (roundTrip func()) {
|
||||
conn := newNonLegacyTestConn(tb)
|
||||
tb.Cleanup(func() { conn.Close() })
|
||||
conn.logf = logger.Discard
|
||||
|
||||
sendConn, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
tb.Cleanup(func() { sendConn.Close() })
|
||||
|
||||
addTestEndpoint(conn, sendConn)
|
||||
|
||||
var dstAddr net.Addr = conn.pconn4.LocalAddr()
|
||||
sendBuf := make([]byte, 1<<10)
|
||||
for i := range sendBuf {
|
||||
sendBuf[i] = 'x'
|
||||
}
|
||||
|
||||
buf := make([]byte, 2<<10)
|
||||
for i := 0; i < b.N; i++ {
|
||||
return func() {
|
||||
if _, err := sendConn.WriteTo(sendBuf, dstAddr); err != nil {
|
||||
b.Fatalf("WriteTo: %v", err)
|
||||
tb.Fatalf("WriteTo: %v", err)
|
||||
}
|
||||
n, ep, err := conn.ReceiveIPv4(buf)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
tb.Fatal(err)
|
||||
}
|
||||
_ = n
|
||||
_ = ep
|
||||
}
|
||||
}
|
||||
|
||||
// goMajorVersion reports the major Go version and whether it is a Tailscale fork.
|
||||
// If parsing fails, goMajorVersion returns 0, false.
|
||||
func goMajorVersion(s string) (version int, isTS bool) {
|
||||
if !strings.HasPrefix(s, "go1.") {
|
||||
return 0, false
|
||||
}
|
||||
mm := s[len("go1."):]
|
||||
var major, rest string
|
||||
for _, sep := range []string{".", "rc", "beta"} {
|
||||
i := strings.Index(mm, sep)
|
||||
if i > 0 {
|
||||
major, rest = mm[:i], mm[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
if major == "" {
|
||||
major = mm
|
||||
}
|
||||
n, err := strconv.Atoi(major)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return n, strings.Contains(rest, "ts")
|
||||
}
|
||||
|
||||
func TestGoMajorVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
version string
|
||||
wantN int
|
||||
wantTS bool
|
||||
}{
|
||||
{"go1.15.8", 15, false},
|
||||
{"go1.16rc1", 16, false},
|
||||
{"go1.16rc1", 16, false},
|
||||
{"go1.15.5-ts3bd89195a3", 15, true},
|
||||
{"go1.15", 15, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
n, ts := goMajorVersion(tt.version)
|
||||
if tt.wantN != n || tt.wantTS != ts {
|
||||
t.Errorf("goMajorVersion(%s) = %v, %v, want %v, %v", tt.version, n, ts, tt.wantN, tt.wantTS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReceiveFromAllocs(t *testing.T) {
|
||||
// Go 1.16 and before: allow 3 allocs.
|
||||
// Go Tailscale fork, Go 1.17+: only allow 2 allocs.
|
||||
major, ts := goMajorVersion(runtime.Version())
|
||||
maxAllocs := 3
|
||||
if major >= 17 || ts {
|
||||
maxAllocs = 2
|
||||
}
|
||||
t.Logf("allowing %d allocs for Go version %q", maxAllocs, runtime.Version())
|
||||
roundTrip := setUpReceiveFrom(t)
|
||||
avg := int(testing.AllocsPerRun(100, roundTrip))
|
||||
if avg > maxAllocs {
|
||||
t.Fatalf("expected %d allocs in ReceiveFrom, got %v", maxAllocs, avg)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReceiveFrom(b *testing.B) {
|
||||
roundTrip := setUpReceiveFrom(b)
|
||||
for i := 0; i < b.N; i++ {
|
||||
roundTrip()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReceiveFrom_Native(b *testing.B) {
|
||||
recvConn, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -1484,19 +1657,3 @@ func BenchmarkReceiveFrom_Native(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func wireguardGoLogger(logf logger.Logf) *device.Logger {
|
||||
// wireguard-go logs as it starts and stops routines.
|
||||
// Silence those; there are a lot of them, and they're just noise.
|
||||
allowLogf := func(s string) bool {
|
||||
return !strings.Contains(s, "Routine:")
|
||||
}
|
||||
filtered := logger.Filtered(logf, allowLogf)
|
||||
stdLogger := logger.StdLogger(filtered)
|
||||
|
||||
return &device.Logger{
|
||||
Debug: stdLogger,
|
||||
Info: stdLogger,
|
||||
Error: stdLogger,
|
||||
}
|
||||
}
|
||||
|
||||
72
wgengine/monitor/monitor_darwin_tailscaled.go
Normal file
72
wgengine/monitor/monitor_darwin_tailscaled.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!redo
|
||||
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// unspecifiedMessage is a minimal message implementation that should not
|
||||
// be ignored. In general, OS-specific implementations should use better
|
||||
// types and avoid this if they can.
|
||||
type unspecifiedMessage struct{}
|
||||
|
||||
func (unspecifiedMessage) ignore() bool { return false }
|
||||
|
||||
func newOSMon(logf logger.Logf) (osMon, error) {
|
||||
return new(routeMonitorSubProcMon), nil
|
||||
}
|
||||
|
||||
// routeMonitorSubProcMon is a very simple (temporary? but I know
|
||||
// better) monitor implementation for darwin in tailscaled-mode where
|
||||
// we can just shell out to "route -n monitor". It waits for any input
|
||||
// but doesn't parse it. Then we poll to see if something is different.
|
||||
type routeMonitorSubProcMon struct {
|
||||
closed syncs.AtomicBool
|
||||
cmd *exec.Cmd // of "/sbin/route -n monitor"
|
||||
br *bufio.Reader
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (m *routeMonitorSubProcMon) Close() error {
|
||||
m.closed.Set(true)
|
||||
if m.cmd != nil {
|
||||
m.cmd.Process.Kill()
|
||||
m.cmd = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *routeMonitorSubProcMon) Receive() (message, error) {
|
||||
if m.closed.Get() {
|
||||
return nil, errors.New("monitor closed")
|
||||
}
|
||||
if m.cmd == nil {
|
||||
cmd := exec.Command("/sbin/route", "-n", "monitor")
|
||||
outPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.br = bufio.NewReader(outPipe)
|
||||
m.cmd = cmd
|
||||
m.buf = make([]byte, 16<<10)
|
||||
}
|
||||
_, err := m.br.Read(m.buf)
|
||||
if err != nil {
|
||||
m.Close()
|
||||
return nil, err
|
||||
}
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user