Compare commits
93 Commits
josh/debug
...
crawshaw/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5166565a7 | ||
|
|
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 | ||
|
|
da4ec54756 | ||
|
|
5c619882bc | ||
|
|
9936cffc1a | ||
|
|
3fa86a8b23 | ||
|
|
4811236189 | ||
|
|
c78ed5b399 | ||
|
|
013da6660e | ||
|
|
8578b0445d | ||
|
|
7c1a9e8616 | ||
|
|
a64d06f15c | ||
|
|
503db5540f | ||
|
|
ed2169ae99 | ||
|
|
12bb949178 | ||
|
|
63af950d8c | ||
|
|
23c2dc2165 | ||
|
|
e23b4191c4 | ||
|
|
0733c5d2e0 | ||
|
|
57d95dd005 | ||
|
|
a2463e8948 | ||
|
|
d456bfdc6d | ||
|
|
2d837f79dc | ||
|
|
08baa17d9a | ||
|
|
7c76435bf7 | ||
|
|
d2529affa2 | ||
|
|
3ad7c2133a | ||
|
|
b560386c1a | ||
|
|
01e8b7fb7e | ||
|
|
5611f290eb | ||
|
|
a45665426b | ||
|
|
420c7a35e2 | ||
|
|
3ac952d4e9 | ||
|
|
a4b39022e0 | ||
|
|
b00c0e5f60 |
@@ -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 .
|
||||
134
cmd/hello/hello.go
Normal file
134
cmd/hello/hello.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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"
|
||||
"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")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
var tmpl = template.Must(template.New("home").Parse(slurpHTML()))
|
||||
|
||||
type tmplData struct {
|
||||
DisplayName string // "Foo Barberson"
|
||||
LoginName string // "foo@bar.com"
|
||||
MachineName string // "imac5k"
|
||||
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
|
||||
}
|
||||
who, err := whoIs(ip)
|
||||
if err != nil {
|
||||
log.Printf("whois(%q) error: %v", ip, err)
|
||||
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl.Execute(w, tmplData{
|
||||
DisplayName: who.UserProfile.DisplayName,
|
||||
LoginName: who.UserProfile.LoginName,
|
||||
MachineName: who.Node.ComputedName,
|
||||
IP: ip,
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return safesocket.ConnectDefault()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func whoIs(ip string) (*tailcfg.WhoIsResponse, error) {
|
||||
res, err := tsSockClient.Get("http://local-tailscaled.sock/localapi/v0/whois?ip=" + url.QueryEscape(ip))
|
||||
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
|
||||
}
|
||||
17
cmd/hello/hello.tmpl.html
Normal file
17
cmd/hello/hello.tmpl.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello from Tailscale</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello!</h1>
|
||||
<p>
|
||||
Hello {{.DisplayName}} ({{.LoginName}}) from {{.MachineName}} ({{.IP}}).
|
||||
</p>
|
||||
<p>
|
||||
<b>Your Tailscale is working!</b>
|
||||
</p>
|
||||
<p>
|
||||
Welcome to Tailscale.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"
|
||||
|
||||
@@ -181,7 +180,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 {
|
||||
@@ -206,19 +205,9 @@ func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
return ps.DNSName[:i]
|
||||
}
|
||||
if ps.DNSName != "" {
|
||||
return ps.DNSName
|
||||
return strings.TrimRight(ps.DNSName, ".")
|
||||
}
|
||||
return fmt.Sprintf("- (%q)", ps.SimpleHostName())
|
||||
}
|
||||
|
||||
func sortKey(ps *ipnstate.PeerStatus) string {
|
||||
if ps.DNSName != "" {
|
||||
return ps.DNSName
|
||||
}
|
||||
if ps.HostName != "" {
|
||||
return ps.HostName
|
||||
}
|
||||
return ps.TailAddr
|
||||
return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_"))
|
||||
}
|
||||
|
||||
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
|
||||
@@ -120,6 +120,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)
|
||||
@@ -139,6 +144,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
@@ -149,8 +155,18 @@ 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()
|
||||
}
|
||||
|
||||
@@ -212,7 +228,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 {
|
||||
|
||||
@@ -27,7 +27,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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
|
||||
@@ -66,6 +65,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 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/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -88,6 +88,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/control/controlclient+
|
||||
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+
|
||||
|
||||
@@ -31,7 +31,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+
|
||||
@@ -103,6 +102,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 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+
|
||||
@@ -112,7 +112,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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
|
||||
@@ -129,6 +129,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/control/controlclient+
|
||||
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+
|
||||
|
||||
@@ -103,10 +103,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")
|
||||
}
|
||||
@@ -140,6 +136,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -744,6 +744,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)
|
||||
@@ -762,6 +770,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
c.mu.Unlock()
|
||||
|
||||
nm := &NetworkMap{
|
||||
SelfNode: resp.Node,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
@@ -790,7 +799,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 +984,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 +1079,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
|
||||
|
||||
@@ -13,18 +13,18 @@ import (
|
||||
"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"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
type NetworkMap struct {
|
||||
// Core networking
|
||||
|
||||
SelfNode *tailcfg.Node
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgkey.Private
|
||||
Expiry time.Time
|
||||
@@ -63,27 +63,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 {
|
||||
@@ -312,12 +301,14 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Bits == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
logf("[v1] wgcfg: not accepting default route from %q (%v)",
|
||||
nodeDebugName(peer), peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)",
|
||||
allowedIP, nodeDebugName(peer), peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -328,6 +319,20 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func nodeDebugName(n *tailcfg.Node) string {
|
||||
name := n.Name
|
||||
if name == "" {
|
||||
name = n.Hostinfo.Hostname
|
||||
}
|
||||
if i := strings.Index(name, "."); i != -1 {
|
||||
name = name[:i]
|
||||
}
|
||||
if name == "" && len(n.Addresses) != 0 {
|
||||
return n.Addresses[0].String()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -24,7 +24,7 @@ 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-20210114205708-a1377e83f551
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
|
||||
12
go.sum
12
go.sum
@@ -288,6 +288,18 @@ github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBW
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e h1:ZXbXfVJOhSq4/Gt7TnqwXBPCctzYXkWXo3oQS7LZ40I=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98 h1:khwYPK1eT+4pmEFyCjpf6Br/0JWjdVT3uQ+ILFJPTRo=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551 h1:hjBVxvVa145kVflAFkVcTr/zwUzBO4SqfSS6xhbcMv8=
|
||||
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/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=
|
||||
|
||||
@@ -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) {
|
||||
|
||||
49
ipn/ipnserver/conn_linux.go
Normal file
49
ipn/ipnserver/conn_linux.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func isReadonlyConn(c net.Conn, logf logger.Logf) (ro bool) {
|
||||
ro = true // conservative default for naked returns below
|
||||
uc, ok := c.(*net.UnixConn)
|
||||
if !ok {
|
||||
logf("unexpected connection type %T", c)
|
||||
return
|
||||
}
|
||||
raw, err := uc.SyscallConn()
|
||||
if err != nil {
|
||||
logf("SyscallConn: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var cred *unix.Ucred
|
||||
cerr := raw.Control(func(fd uintptr) {
|
||||
cred, err = unix.GetsockoptUcred(int(fd),
|
||||
unix.SOL_SOCKET,
|
||||
unix.SO_PEERCRED)
|
||||
})
|
||||
if cerr != nil {
|
||||
logf("raw.Control: %v", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logf("raw.Control: %v", err)
|
||||
return
|
||||
}
|
||||
if cred.Uid == 0 {
|
||||
// root is not read-only.
|
||||
return false
|
||||
}
|
||||
logf("non-root connection from %v (read-only)", cred.Uid)
|
||||
return true
|
||||
}
|
||||
27
ipn/ipnserver/conn_no_ucred.go
Normal file
27
ipn/ipnserver/conn_no_ucred.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 !linux
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func isReadonlyConn(c net.Conn, logf logger.Logf) bool {
|
||||
// Windows doesn't need/use this mechanism, at least yet. It
|
||||
// has a different last-user-wins auth model.
|
||||
|
||||
// And on Darwin, we're not using it yet, as the Darwin
|
||||
// tailscaled port isn't yet done, and unix.Ucred and
|
||||
// unix.GetsockoptUcred aren't in x/sys/unix.
|
||||
|
||||
// TODO(bradfitz): OpenBSD and FreeBSD should implement this too.
|
||||
// But their x/sys/unix package is different than Linux, so
|
||||
// I didn't include it for now.
|
||||
return false
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package ipnserver
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
"tailscale.com/util/systemd"
|
||||
@@ -113,10 +115,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 +128,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 {
|
||||
@@ -268,6 +273,10 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("[v1] incoming control connection")
|
||||
|
||||
if isReadonlyConn(c, logf) {
|
||||
ctx = ipn.ReadonlyContextOf(ctx)
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(br)
|
||||
if err != nil {
|
||||
@@ -279,7 +288,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
return
|
||||
}
|
||||
s.bsMu.Lock()
|
||||
if err := s.bs.GotCommandMsg(msg); err != nil {
|
||||
if err := s.bs.GotCommandMsg(ctx, msg); err != nil {
|
||||
logf("GotCommandMsg: %v", err)
|
||||
}
|
||||
gotQuit := s.bs.GotQuit
|
||||
@@ -355,7 +364,7 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
if doReset {
|
||||
s.logf("identity changed; resetting server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}()
|
||||
@@ -407,7 +416,7 @@ func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -499,41 +508,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)
|
||||
@@ -562,6 +536,82 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
store = &ipn.MemoryStore{}
|
||||
}
|
||||
|
||||
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 := ipn.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
@@ -575,13 +625,14 @@ 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)
|
||||
})
|
||||
opts.DebugMux.Handle("/localapi/v0/whois", whoIsHandler{b})
|
||||
}
|
||||
|
||||
server.b = b
|
||||
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
||||
|
||||
if opts.AutostartStateKey != "" {
|
||||
server.bs.GotCommand(&ipn.Command{
|
||||
server.bs.GotCommand(context.TODO(), &ipn.Command{
|
||||
Version: version.Long,
|
||||
Start: &ipn.StartArgs{
|
||||
Opts: ipn.Options{
|
||||
@@ -752,6 +803,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
|
||||
@@ -794,6 +866,10 @@ 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 && r.URL.Path == "/localapi/v0/whois" {
|
||||
whoIsHandler{s.b}.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
|
||||
@@ -817,3 +893,40 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// whoIsHandler is the debug server's /debug?ip=$IP HTTP handler.
|
||||
type whoIsHandler struct {
|
||||
b *ipn.LocalBackend
|
||||
}
|
||||
|
||||
func (h whoIsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -274,13 +281,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 +312,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 +395,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
|
||||
}
|
||||
|
||||
57
ipn/local.go
57
ipn/local.go
@@ -15,7 +15,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
@@ -37,6 +36,7 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
var controlDebugFlags = getControlDebugFlags()
|
||||
@@ -90,6 +90,7 @@ type LocalBackend struct {
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
netMap *controlclient.NetworkMap
|
||||
nodeByAddr map[netaddr.IP]*tailcfg.Node
|
||||
activeLogin string // last logged LoginName from netMap
|
||||
engineStatus EngineStatus
|
||||
endpoints []string
|
||||
@@ -234,7 +235,22 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -562,13 +578,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)")
|
||||
var prevFilter *filter.Filter // don't reuse old filter state
|
||||
b.e.SetFilter(filter.New(nil, localNets, prevFilter, 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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1507,6 +1523,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
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -20,6 +21,24 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
type readOnlyContextKey struct{}
|
||||
|
||||
// IsReadonlyContext reports whether ctx is a read-only context, as currently used
|
||||
// by Unix non-root users running the "tailscale" CLI command. They can run "status",
|
||||
// but not much else.
|
||||
func IsReadonlyContext(ctx context.Context) bool {
|
||||
return ctx.Value(readOnlyContextKey{}) != nil
|
||||
}
|
||||
|
||||
// ReadonlyContextOf returns ctx wrapped with a context value that
|
||||
// will make IsReadonlyContext reports true.
|
||||
func ReadonlyContextOf(ctx context.Context) context.Context {
|
||||
if IsReadonlyContext(ctx) {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, readOnlyContextKey{}, readOnlyContextKey{})
|
||||
}
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
type NoArgs struct{}
|
||||
@@ -111,7 +130,7 @@ func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
|
||||
|
||||
// GotCommandMsg parses the incoming message b as a JSON Command and
|
||||
// calls GotCommand with it.
|
||||
func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
func (bs *BackendServer) GotCommandMsg(ctx context.Context, b []byte) error {
|
||||
cmd := &Command{}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
@@ -119,15 +138,19 @@ func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
if err := json.Unmarshal(b, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return bs.GotCommand(cmd)
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
|
||||
func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error {
|
||||
cmd.Version = version.Long
|
||||
return bs.GotCommand(cmd)
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
// 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",
|
||||
cmd.Version, version.Long)
|
||||
@@ -141,12 +164,33 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(bradfitz): finish plumbing context down to all the methods below;
|
||||
// currently we just check for read-only contexts in this method and
|
||||
// then never use contexts again.
|
||||
|
||||
// Actions permitted with a read-only context:
|
||||
if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.RequestStatus; c != nil {
|
||||
bs.b.RequestStatus()
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
}
|
||||
|
||||
if IsReadonlyContext(ctx) {
|
||||
msg := ErrMsgPermissionDenied
|
||||
bs.send(Notify{ErrMessage: &msg})
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd.Quit != nil {
|
||||
bs.GotQuit = true
|
||||
return errors.New("Quit command received")
|
||||
}
|
||||
|
||||
if c := cmd.Start; c != nil {
|
||||
} else if c := cmd.Start; c != nil {
|
||||
opts := c.Opts
|
||||
opts.Notify = bs.send
|
||||
return bs.b.Start(opts)
|
||||
@@ -165,27 +209,17 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.SetWantRunning; c != nil {
|
||||
bs.b.SetWantRunning(*c)
|
||||
return nil
|
||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.RequestStatus; c != nil {
|
||||
bs.b.RequestStatus()
|
||||
return nil
|
||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||
bs.b.FakeExpireAfter(c.Duration)
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
|
||||
func (bs *BackendServer) Reset() error {
|
||||
func (bs *BackendServer) Reset(ctx context.Context) error {
|
||||
// Tell the backend we got a Logout command, which will cause it
|
||||
// to forget all its authentication information.
|
||||
return bs.GotFakeCommand(&Command{Logout: &NoArgs{}})
|
||||
return bs.GotFakeCommand(ctx, &Command{Logout: &NoArgs{}})
|
||||
}
|
||||
|
||||
type BackendClient struct {
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -81,7 +82,7 @@ func TestClientServer(t *testing.T) {
|
||||
serverToClientCh <- append([]byte{}, b...)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
bs.GotCommandMsg(b)
|
||||
bs.GotCommandMsg(context.TODO(), b)
|
||||
}
|
||||
slogf := func(fmt string, args ...interface{}) {
|
||||
t.Logf("s: "+fmt, args...)
|
||||
|
||||
@@ -6,8 +6,12 @@ package logtail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -23,28 +27,201 @@ func TestFastShutdown(t *testing.T) {
|
||||
l := NewLogger(Config{
|
||||
BaseURL: testServ.URL,
|
||||
}, t.Logf)
|
||||
l.Shutdown(ctx)
|
||||
err := l.Shutdown(ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadMessages(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
uploads := 0
|
||||
testServ := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
uploads += 1
|
||||
}))
|
||||
defer testServ.Close()
|
||||
// maximum number of times a test will call l.Write()
|
||||
const logLines = 3
|
||||
|
||||
l := NewLogger(Config{BaseURL: testServ.URL}, t.Logf)
|
||||
for i := 1; i < 10; i++ {
|
||||
type LogtailTestServer struct {
|
||||
srv *httptest.Server // Log server
|
||||
uploaded chan []byte
|
||||
}
|
||||
|
||||
func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
|
||||
ts := LogtailTestServer{}
|
||||
|
||||
// max channel backlog = 1 "started" + #logLines x "log line" + 1 "closed"
|
||||
ts.uploaded = make(chan []byte, 2+logLines)
|
||||
|
||||
ts.srv = httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Error("failed to read HTTP request")
|
||||
}
|
||||
ts.uploaded <- body
|
||||
}))
|
||||
|
||||
t.Cleanup(ts.srv.Close)
|
||||
|
||||
l := NewLogger(Config{BaseURL: ts.srv.URL}, t.Logf)
|
||||
|
||||
// There is always an initial "logtail started" message
|
||||
body := <-ts.uploaded
|
||||
if !strings.Contains(string(body), "started") {
|
||||
t.Errorf("unknown start logging statement: %q", string(body))
|
||||
}
|
||||
|
||||
return &ts, l
|
||||
}
|
||||
|
||||
func TestDrainPendingMessages(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(t)
|
||||
|
||||
for i := 0; i < logLines; i++ {
|
||||
l.Write([]byte("log line"))
|
||||
}
|
||||
|
||||
l.Shutdown(ctx)
|
||||
cancel()
|
||||
if uploads == 0 {
|
||||
t.Error("no log uploads")
|
||||
// all of the "log line" messages usually arrive at once, but poll if needed.
|
||||
body := ""
|
||||
for i := 0; i <= logLines; i++ {
|
||||
body += string(<-ts.uploaded)
|
||||
count := strings.Count(body, "log line")
|
||||
if count == logLines {
|
||||
break
|
||||
}
|
||||
// if we never find count == logLines, the test will eventually time out.
|
||||
}
|
||||
|
||||
err := l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeAndUploadMessages(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
log string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"plain text",
|
||||
"log line",
|
||||
"log line",
|
||||
},
|
||||
{
|
||||
"simple JSON",
|
||||
`{"text": "log line"}`,
|
||||
"log line",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
io.WriteString(l, tt.log)
|
||||
body := <-ts.uploaded
|
||||
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
got := data["text"]
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
|
||||
}
|
||||
|
||||
ltail, ok := data["logtail"]
|
||||
if ok {
|
||||
logtailmap := ltail.(map[string]interface{})
|
||||
_, ok = logtailmap["client_time"]
|
||||
if !ok {
|
||||
t.Errorf("%s: no client_time present", tt.name)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%s: no logtail map present", tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
err := l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeSpecialCases(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(t)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// JSON log message already contains a logtail field.
|
||||
io.WriteString(l, `{"logtail": "LOGTAIL", "text": "text"}`)
|
||||
body := <-ts.uploaded
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
errorHasLogtail, ok := data["error_has_logtail"]
|
||||
if ok {
|
||||
if errorHasLogtail != "LOGTAIL" {
|
||||
t.Errorf("error_has_logtail: got:%q; want:%q",
|
||||
errorHasLogtail, "LOGTAIL")
|
||||
}
|
||||
} else {
|
||||
t.Errorf("no error_has_logtail field: %v", data)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// special characters
|
||||
io.WriteString(l, "\b\f\n\r\t"+`"\`)
|
||||
bodytext := string(<-ts.uploaded)
|
||||
// json.Unmarshal would unescape the characters, we have to look at the encoded text
|
||||
escaped := strings.Contains(bodytext, `\b\f\n\r\t\"\`)
|
||||
if !escaped {
|
||||
t.Errorf("special characters got %s", bodytext)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// skipClientTime to omit the logtail metadata
|
||||
l.skipClientTime = true
|
||||
io.WriteString(l, "text")
|
||||
body = <-ts.uploaded
|
||||
data = make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, ok = data["logtail"]
|
||||
if ok {
|
||||
t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// lowMem + long string
|
||||
l.skipClientTime = false
|
||||
l.lowMem = true
|
||||
longStr := strings.Repeat("0", 512)
|
||||
io.WriteString(l, longStr)
|
||||
body = <-ts.uploaded
|
||||
data = make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
text, ok := data["text"]
|
||||
if !ok {
|
||||
t.Errorf("lowMem: no text %v", data)
|
||||
}
|
||||
if n := len(text.(string)); n > 300 {
|
||||
t.Errorf("lowMem: got %d chars; want <300 chars", n)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
err = l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,3 +252,54 @@ func TestLoggerWriteLength(t *testing.T) {
|
||||
t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndRemoveLogLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
log string
|
||||
wantLevel int
|
||||
wantLog string
|
||||
}{
|
||||
{
|
||||
"no level",
|
||||
0,
|
||||
"no level",
|
||||
},
|
||||
{
|
||||
"[v1] level 1",
|
||||
1,
|
||||
"level 1",
|
||||
},
|
||||
{
|
||||
"level 1 [v1] ",
|
||||
1,
|
||||
"level 1 ",
|
||||
},
|
||||
{
|
||||
"[v2] level 2",
|
||||
2,
|
||||
"level 2",
|
||||
},
|
||||
{
|
||||
"level [v2] 2",
|
||||
2,
|
||||
"level 2",
|
||||
},
|
||||
{
|
||||
"[v3] no level 3",
|
||||
0,
|
||||
"[v3] no level 3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
gotLevel, gotLog := parseAndRemoveLogLevel([]byte(tt.log))
|
||||
if gotLevel != tt.wantLevel {
|
||||
t.Errorf("parseAndRemoveLogLevel(%q): got:%d; want %d",
|
||||
tt.log, gotLevel, tt.wantLevel)
|
||||
}
|
||||
if string(gotLog) != tt.wantLog {
|
||||
t.Errorf("parseAndRemoveLogLevel(%q): got:%q; want %q",
|
||||
tt.log, gotLog, tt.wantLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -60,7 +60,7 @@ func (p *Pipe) Read(b []byte) (n int, err error) {
|
||||
for {
|
||||
p.mu.Lock()
|
||||
closed := p.closed
|
||||
timedout := !p.readTimeout.IsZero() && time.Now().After(p.readTimeout)
|
||||
timedout := !p.readTimeout.IsZero() && !time.Now().Before(p.readTimeout)
|
||||
blocked := p.blocked
|
||||
if !closed && !timedout && len(p.buf) > 0 {
|
||||
n2 := copy(b, p.buf)
|
||||
@@ -99,7 +99,7 @@ func (p *Pipe) Write(b []byte) (n int, err error) {
|
||||
for {
|
||||
p.mu.Lock()
|
||||
closed := p.closed
|
||||
timedout := !p.writeTimeout.IsZero() && time.Now().After(p.writeTimeout)
|
||||
timedout := !p.writeTimeout.IsZero() && !time.Now().Before(p.writeTimeout)
|
||||
blocked := p.blocked
|
||||
if !closed && !timedout {
|
||||
n2 := len(b)
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
p := NewPipe("p1", 1<<16)
|
||||
p.SetWriteDeadline(time.Now().Add(-1 * time.Second))
|
||||
n, err := p.Write([]byte{'h'})
|
||||
if err == nil || !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
if !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
t.Errorf("missing write timeout got err: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
@@ -49,7 +49,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
p.SetReadDeadline(time.Now().Add(-1 * time.Second))
|
||||
b := make([]byte, 1)
|
||||
n, err := p.Read(b)
|
||||
if err == nil || !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
if !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
t.Errorf("missing read timeout got err: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
@@ -65,7 +65,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
if err := p.Block(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := p.Write([]byte{'h'}); err == nil || !errors.Is(err, ErrWriteTimeout) {
|
||||
if _, err := p.Write([]byte{'h'}); !errors.Is(err, ErrWriteTimeout) {
|
||||
t.Fatalf("want write timeout got: %v", err)
|
||||
}
|
||||
})
|
||||
@@ -80,11 +80,10 @@ func TestPipeTimeout(t *testing.T) {
|
||||
if err := p.Block(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := p.Read(b); err == nil || !errors.Is(err, ErrReadTimeout) {
|
||||
if _, err := p.Read(b); !errors.Is(err, ErrReadTimeout) {
|
||||
t.Fatalf("want read timeout got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestLimit(t *testing.T) {
|
||||
@@ -117,4 +116,8 @@ func TestLimit(t *testing.T) {
|
||||
} else if n != 1 {
|
||||
t.Errorf("Read(%q): n=%d want 1", string(b), n)
|
||||
}
|
||||
|
||||
if err := <-errCh; err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,6 @@ type Header interface {
|
||||
// purpose of computing length and checksum fields. Marshal
|
||||
// implementations must not allocate memory.
|
||||
Marshal(buf []byte) error
|
||||
// ToResponse transforms the header into one for a response packet.
|
||||
// For instance, this swaps the source and destination IPs.
|
||||
ToResponse()
|
||||
}
|
||||
|
||||
// Generate generates a new packet with the given Header and
|
||||
|
||||
@@ -24,6 +24,17 @@ const (
|
||||
TCP IPProto = 0x06
|
||||
UDP IPProto = 0x11
|
||||
|
||||
// TSMP is the Tailscale Message Protocol (our ICMP-ish
|
||||
// thing), an IP protocol used only between Tailscale nodes
|
||||
// (still encrypted by WireGuard) that communicates why things
|
||||
// failed, etc.
|
||||
//
|
||||
// Proto number 99 is reserved for "any private encryption
|
||||
// scheme". We never accept these from the host OS stack nor
|
||||
// send them to the host network stack. It's only used between
|
||||
// nodes.
|
||||
TSMP IPProto = 99
|
||||
|
||||
// Fragment represents any non-first IP fragment, for which we
|
||||
// don't have the sub-protocol header (and therefore can't
|
||||
// figure out what the sub-protocol is).
|
||||
@@ -47,6 +58,8 @@ func (p IPProto) String() string {
|
||||
return "UDP"
|
||||
case TCP:
|
||||
return "TCP"
|
||||
case TSMP:
|
||||
return "TSMP"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -204,6 +204,10 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
return
|
||||
case TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
@@ -291,6 +295,10 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
case TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
|
||||
@@ -274,7 +274,38 @@ var igmpPacketDecode = Parsed{
|
||||
Dst: mustIPPort("224.0.0.251:0"),
|
||||
}
|
||||
|
||||
func TestParsed(t *testing.T) {
|
||||
var ipv4TSMPBuffer = []byte{
|
||||
// IPv4 header:
|
||||
0x45, 0x00,
|
||||
0x00, 0x1b, // 20 + 7 bytes total
|
||||
0x00, 0x00, // ID
|
||||
0x00, 0x00, // Fragment
|
||||
0x40, // TTL
|
||||
byte(TSMP),
|
||||
0x5f, 0xc3, // header checksum (wrong here)
|
||||
// source IP:
|
||||
0x64, 0x5e, 0x0c, 0x0e,
|
||||
// dest IP:
|
||||
0x64, 0x4a, 0x46, 0x03,
|
||||
byte(TSMPTypeRejectedConn),
|
||||
byte(TCP),
|
||||
byte(RejectedDueToACLs),
|
||||
0x00, 123, // src port
|
||||
0x00, 80, // dst port
|
||||
}
|
||||
|
||||
var ipv4TSMPDecode = Parsed{
|
||||
b: ipv4TSMPBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 20,
|
||||
length: 27,
|
||||
IPVersion: 4,
|
||||
IPProto: TSMP,
|
||||
Src: mustIPPort("100.94.12.14:0"),
|
||||
Dst: mustIPPort("100.74.70.3:0"),
|
||||
}
|
||||
|
||||
func TestParsedString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
qdecode Parsed
|
||||
@@ -288,6 +319,7 @@ func TestParsed(t *testing.T) {
|
||||
{"icmp6", icmp6PacketDecode, "ICMPv6{[fe80::fb57:1dea:9c39:8fb7]:0 > [ff02::2]:0}"},
|
||||
{"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"},
|
||||
{"unknown", unknownPacketDecode, "Unknown{???}"},
|
||||
{"ipv4_tsmp", ipv4TSMPDecode, "TSMP{100.94.12.14:0 > 100.74.70.3:0}"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -324,6 +356,7 @@ func TestDecode(t *testing.T) {
|
||||
{"igmp", igmpPacketBuffer, igmpPacketDecode},
|
||||
{"unknown", unknownPacketBuffer, unknownPacketDecode},
|
||||
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
|
||||
{"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -331,7 +364,7 @@ func TestDecode(t *testing.T) {
|
||||
var got Parsed
|
||||
got.Decode(tt.buf)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mismatch\n got: %#v\nwant: %#v", got, tt.want)
|
||||
t.Errorf("mismatch\n got: %s %#v\nwant: %s %#v", got.String(), got, tt.want.String(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -416,9 +449,16 @@ func TestMarshalResponse(t *testing.T) {
|
||||
icmpHeader := icmp4RequestDecode.ICMP4Header()
|
||||
udpHeader := udp4RequestDecode.UDP4Header()
|
||||
|
||||
type HeaderToResponser interface {
|
||||
Header
|
||||
// ToResponse transforms the header into one for a response packet.
|
||||
// For instance, this swaps the source and destination IPs.
|
||||
ToResponse()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
header Header
|
||||
header HeaderToResponser
|
||||
want []byte
|
||||
}{
|
||||
{"icmp", &icmpHeader, icmp4ReplyBuffer},
|
||||
|
||||
140
net/packet/tsmp.go
Normal file
140
net/packet/tsmp.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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.
|
||||
|
||||
// TSMP is our ICMP-like "Tailscale Message Protocol" for signaling
|
||||
// Tailscale-specific messages between nodes. It uses IP protocol 99
|
||||
// (reserved for "any private encryption scheme") within
|
||||
// Wireguard's normal encryption between peers and never hits the host
|
||||
// network stack.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/flowtrack"
|
||||
)
|
||||
|
||||
// TailscaleRejectedHeader is a TSMP message that says that one
|
||||
// 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:
|
||||
// * '!'
|
||||
// * IPProto byte (IANA protocol number: TCP or UDP)
|
||||
// * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
|
||||
// * srcPort big endian uint16
|
||||
// * dstPort big endian uint16
|
||||
//
|
||||
// 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.
|
||||
type TailscaleRejectedHeader struct {
|
||||
IPSrc netaddr.IP // IPv4 or IPv6 header's src IP
|
||||
IPDst netaddr.IP // IPv4 or IPv6 header's dst IP
|
||||
Src netaddr.IPPort // rejected flow's src
|
||||
Dst netaddr.IPPort // rejected flow's dst
|
||||
Proto IPProto // proto that was rejected (TCP or UDP)
|
||||
Reason TailscaleRejectReason // why the connection was rejected
|
||||
}
|
||||
|
||||
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
|
||||
return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst}
|
||||
}
|
||||
|
||||
func (rh TailscaleRejectedHeader) String() string {
|
||||
return fmt.Sprintf("TSMP-reject-flow{%s %s > %s}: %s", rh.Proto, rh.Src, rh.Dst, rh.Reason)
|
||||
}
|
||||
|
||||
type TSMPType uint8
|
||||
|
||||
const (
|
||||
TSMPTypeRejectedConn TSMPType = '!'
|
||||
)
|
||||
|
||||
type TailscaleRejectReason byte
|
||||
|
||||
const (
|
||||
RejectedDueToACLs TailscaleRejectReason = 'A'
|
||||
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
|
||||
)
|
||||
|
||||
func (r TailscaleRejectReason) String() string {
|
||||
switch r {
|
||||
case RejectedDueToACLs:
|
||||
return "acl"
|
||||
case RejectedDueToShieldsUp:
|
||||
return "shields"
|
||||
}
|
||||
return fmt.Sprintf("0x%02x", byte(r))
|
||||
}
|
||||
|
||||
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
|
||||
1 + // IPProto byte
|
||||
1 + // TailscaleRejectReason byte
|
||||
2*2 // 2 uint16 ports
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
if h.Src.IP.Is4() {
|
||||
iph := IP4Header{
|
||||
IPProto: TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip4HeaderLength:]
|
||||
} else if h.Src.IP.Is6() {
|
||||
iph := IP6Header{
|
||||
IPProto: TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip6HeaderLength:]
|
||||
} else {
|
||||
return errors.New("bogus src IP")
|
||||
}
|
||||
buf[0] = byte(TSMPTypeRejectedConn)
|
||||
buf[1] = byte(h.Proto)
|
||||
buf[2] = byte(h.Reason)
|
||||
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
|
||||
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsTailscaleRejectedHeader parses pp as an incoming rejection
|
||||
// connection TSMP message.
|
||||
//
|
||||
// ok reports whether pp was a valid TSMP rejection packet.
|
||||
func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok bool) {
|
||||
p := pp.Payload()
|
||||
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
|
||||
return
|
||||
}
|
||||
return 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
|
||||
}
|
||||
63
net/packet/tsmp_test.go
Normal file
63
net/packet/tsmp_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 packet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestTailscaleRejectedHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
h TailscaleRejectedHeader
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("5.5.5.5"),
|
||||
IPDst: netaddr.MustParseIP("1.2.3.4"),
|
||||
Src: netaddr.MustParseIPPort("1.2.3.4:567"),
|
||||
Dst: netaddr.MustParseIPPort("5.5.5.5:443"),
|
||||
Proto: TCP,
|
||||
Reason: RejectedDueToACLs,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{TCP 1.2.3.4:567 > 5.5.5.5:443}: acl",
|
||||
},
|
||||
{
|
||||
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: RejectedDueToShieldsUp,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
gotStr := tt.h.String()
|
||||
if gotStr != tt.wantStr {
|
||||
t.Errorf("%v. String = %q; want %q", i, gotStr, tt.wantStr)
|
||||
continue
|
||||
}
|
||||
pkt := make([]byte, tt.h.Len())
|
||||
tt.h.Marshal(pkt)
|
||||
|
||||
var p Parsed
|
||||
p.Decode(pkt)
|
||||
t.Logf("Parsed: %+v", p)
|
||||
t.Logf("Parsed: %s", p.String())
|
||||
back, ok := p.AsTailscaleRejectedHeader()
|
||||
if !ok {
|
||||
t.Errorf("%v. %q (%02x) didn't parse back", i, gotStr, pkt)
|
||||
continue
|
||||
}
|
||||
if back != tt.h {
|
||||
t.Errorf("%v. %q parsed back as %q", i, tt.h, back)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,9 @@
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type closeable interface {
|
||||
@@ -27,6 +29,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("/var/run/tailscale/tailscaled.sock", 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 +45,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,17 +7,15 @@
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -59,15 +57,58 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
|
||||
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, 0600)
|
||||
os.Chmod(path, perm)
|
||||
return pipe, 0, err
|
||||
}
|
||||
|
||||
// socketPermissionsForOS returns the permissions to use for the
|
||||
// tailscaled.sock.
|
||||
func socketPermissionsForOS() os.FileMode {
|
||||
if runtime.GOOS == "linux" {
|
||||
// On Linux, the ipn/ipnserver package looks at the Unix peer creds
|
||||
// and only permits read-only actions from non-root users, so we want
|
||||
// this opened up wider.
|
||||
//
|
||||
// TODO(bradfitz): unify this all one in place probably, moving some
|
||||
// of ipnserver (which does much of the "safe" bits) here. Maybe
|
||||
// instead of net.Listener, we should return a type that returns
|
||||
// an identity in addition to a net.Conn? (returning a wrapped net.Conn
|
||||
// would surprise downstream callers probably)
|
||||
//
|
||||
// TODO(bradfitz): if OpenBSD and FreeBSD do the equivalent peercreds
|
||||
// stuff that's in ipn/ipnserver/conn_ucred.go, they should also
|
||||
// return 0666 here.
|
||||
return 0666
|
||||
}
|
||||
// Otherwise, root only.
|
||||
return 0600
|
||||
}
|
||||
|
||||
// connectMacOSAppSandbox connects to the Tailscale Network Extension,
|
||||
// which is necessarily running within the macOS App Sandbox. Our
|
||||
// little dance to connect a regular user binary to the sandboxed
|
||||
@@ -123,42 +164,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,8 +634,6 @@ 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"`
|
||||
@@ -638,6 +739,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.
|
||||
@@ -781,6 +887,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 &&
|
||||
@@ -795,7 +902,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 {
|
||||
@@ -825,3 +935,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},
|
||||
|
||||
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...)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,3 +192,27 @@ func Filtered(logf Logf, allow func(s string) bool) Logf {
|
||||
logf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogfCloser wraps logf to create a logger that can be closed.
|
||||
// Calling close makes all future calls to newLogf into no-ops.
|
||||
func LogfCloser(logf Logf) (newLogf Logf, close func()) {
|
||||
var (
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
)
|
||||
close = func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
closed = true
|
||||
}
|
||||
newLogf = func(msg string, args ...interface{}) {
|
||||
mu.Lock()
|
||||
if closed {
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
mu.Unlock()
|
||||
logf(msg, args...)
|
||||
}
|
||||
return newLogf, close
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ type Filter struct {
|
||||
// to an outbound connection that this node made, even if those
|
||||
// incoming packets don't get accepted by matches above.
|
||||
state *filterState
|
||||
|
||||
shieldsUp bool
|
||||
}
|
||||
|
||||
// filterState is a state cache of past seen packets.
|
||||
@@ -54,15 +56,18 @@ const lruMax = 512
|
||||
type Response int
|
||||
|
||||
const (
|
||||
Drop Response = iota // do not continue processing packet.
|
||||
Accept // continue processing packet.
|
||||
noVerdict // no verdict yet, continue running filter
|
||||
Drop Response = iota // do not continue processing packet.
|
||||
DropSilently // do not continue processing packet, but also don't log
|
||||
Accept // continue processing packet.
|
||||
noVerdict // no verdict yet, continue running filter
|
||||
)
|
||||
|
||||
func (r Response) String() string {
|
||||
switch r {
|
||||
case Drop:
|
||||
return "Drop"
|
||||
case DropSilently:
|
||||
return "DropSilently"
|
||||
case Accept:
|
||||
return "Accept"
|
||||
case noVerdict:
|
||||
@@ -72,6 +77,10 @@ func (r Response) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (r Response) IsDrop() bool {
|
||||
return r == Drop || r == DropSilently
|
||||
}
|
||||
|
||||
// RunFlags controls the filter's debug log verbosity at runtime.
|
||||
type RunFlags int
|
||||
|
||||
@@ -123,6 +132,20 @@ func NewAllowNone(logf logger.Logf) *Filter {
|
||||
return 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
|
||||
}
|
||||
|
||||
// New creates a new packet filter. The filter enforces that incoming
|
||||
// packets must be destined to an IP in localNets, and must be allowed
|
||||
// by matches. If shareStateWith is non-nil, the returned filter
|
||||
@@ -253,6 +276,10 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response {
|
||||
return f.RunIn(pkt, 0)
|
||||
}
|
||||
|
||||
// ShieldsUp reports whether this is a "shields up" (block everything
|
||||
// incoming) filter.
|
||||
func (f *Filter) ShieldsUp() bool { return f.shieldsUp }
|
||||
|
||||
// RunIn determines whether this node is allowed to receive q from a
|
||||
// Tailscale peer.
|
||||
func (f *Filter) RunIn(q *packet.Parsed, rf RunFlags) Response {
|
||||
@@ -339,6 +366,8 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||
if f.matches4.match(q) {
|
||||
return Accept, "udp ok"
|
||||
}
|
||||
case packet.TSMP:
|
||||
return Accept, "tsmp ok"
|
||||
default:
|
||||
return Drop, "Unknown proto"
|
||||
}
|
||||
|
||||
@@ -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,11 +27,19 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
var errNoDestinations = errors.New("magicsock: no destinations")
|
||||
var (
|
||||
errNoDestinations = errors.New("magicsock: no destinations")
|
||||
errDisabled = errors.New("magicsock: legacy networking disabled")
|
||||
)
|
||||
|
||||
func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.Endpoint, error) {
|
||||
if c.disableLegacy {
|
||||
return nil, errDisabled
|
||||
}
|
||||
|
||||
a := &addrSet{
|
||||
Logf: c.logf,
|
||||
publicKey: pk,
|
||||
@@ -78,6 +85,10 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End
|
||||
}
|
||||
|
||||
func (c *Conn) findLegacyEndpointLocked(ipp netaddr.IPPort, addr *net.UDPAddr, 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)
|
||||
@@ -139,6 +150,10 @@ func (c *Conn) resetAddrSetStatesLocked() {
|
||||
}
|
||||
|
||||
func (c *Conn) sendAddrSet(b []byte, as *addrSet) error {
|
||||
if c.disableLegacy {
|
||||
return errDisabled
|
||||
}
|
||||
|
||||
var addrBuf [8]netaddr.IPPort
|
||||
dsts, roamAddr := as.appendDests(addrBuf[:0], b)
|
||||
|
||||
@@ -423,8 +438,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.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, ",")
|
||||
}
|
||||
func (a *addrSet) DstIP() net.IP {
|
||||
return a.dst().IP.IPAddr().IP // TODO: add netaddr accessor to cut an alloc here?
|
||||
@@ -433,10 +457,6 @@ func (a *addrSet) SrcIP() net.IP { return nil }
|
||||
func (a *addrSet) SrcToString() string { return "" }
|
||||
func (a *addrSet) ClearSrc() {}
|
||||
|
||||
func (a *addrSet) UpdateDst(new *net.UDPAddr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -567,20 +587,6 @@ func (as *addrSet) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +11,6 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -21,6 +20,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -28,7 +28,6 @@ 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"
|
||||
@@ -46,6 +45,8 @@ import (
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/tstun"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wglog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -127,12 +128,13 @@ 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
|
||||
// friends. You need to call conn.SetNetworkMap and dev.Reconfig
|
||||
// before anything interesting happens.
|
||||
func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, derpMap *tailcfg.DERPMap) *magicStack {
|
||||
func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, derpMap *tailcfg.DERPMap, disableLegacy bool) *magicStack {
|
||||
t.Helper()
|
||||
|
||||
privateKey, err := wgkey.NewPrivate()
|
||||
@@ -147,7 +149,8 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
|
||||
EndpointsFunc: func(eps []string) {
|
||||
epCh <- eps
|
||||
},
|
||||
SimulatedNetwork: l != nettype.Std{},
|
||||
SimulatedNetwork: l != nettype.Std{},
|
||||
DisableLegacyNetworking: disableLegacy,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("constructing magicsock: %v", err)
|
||||
@@ -162,12 +165,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: &device.Logger{
|
||||
Debug: logger.StdLogger(logf),
|
||||
Info: logger.StdLogger(logf),
|
||||
Error: logger.StdLogger(logf),
|
||||
},
|
||||
Logger: wgLogger.DeviceLogger,
|
||||
CreateEndpoint: conn.CreateEndpoint,
|
||||
CreateBind: conn.CreateBind,
|
||||
SkipBindUpdate: true,
|
||||
@@ -190,9 +190,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()
|
||||
@@ -293,7 +299,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
@@ -340,9 +346,10 @@ func TestNewConn(t *testing.T) {
|
||||
|
||||
port := pickPort(t)
|
||||
conn, err := NewConn(Options{
|
||||
Port: port,
|
||||
EndpointsFunc: epFunc,
|
||||
Logf: t.Logf,
|
||||
Port: port,
|
||||
EndpointsFunc: epFunc,
|
||||
Logf: t.Logf,
|
||||
DisableLegacyNetworking: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -355,7 +362,7 @@ func TestNewConn(t *testing.T) {
|
||||
go func() {
|
||||
var pkt [64 << 10]byte
|
||||
for {
|
||||
_, _, _, err := conn.ReceiveIPv4(pkt[:])
|
||||
_, _, err := conn.ReceiveIPv4(pkt[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -516,8 +523,9 @@ func TestDeviceStartStop(t *testing.T) {
|
||||
defer rc.Assert(t)
|
||||
|
||||
conn, err := NewConn(Options{
|
||||
EndpointsFunc: func(eps []string) {},
|
||||
Logf: t.Logf,
|
||||
EndpointsFunc: func(eps []string) {},
|
||||
Logf: t.Logf,
|
||||
DisableLegacyNetworking: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -527,11 +535,7 @@ func TestDeviceStartStop(t *testing.T) {
|
||||
|
||||
tun := tuntest.NewChannelTUN()
|
||||
dev := device.NewDevice(tun.TUN(), &device.DeviceOptions{
|
||||
Logger: &device.Logger{
|
||||
Debug: logger.StdLogger(t.Logf),
|
||||
Info: logger.StdLogger(t.Logf),
|
||||
Error: logger.StdLogger(t.Logf),
|
||||
},
|
||||
Logger: wglog.NewLogger(t.Logf).DeviceLogger,
|
||||
CreateEndpoint: conn.CreateEndpoint,
|
||||
CreateBind: conn.CreateBind,
|
||||
SkipBindUpdate: true,
|
||||
@@ -540,93 +544,6 @@ func TestDeviceStartStop(t *testing.T) {
|
||||
dev.Close()
|
||||
}
|
||||
|
||||
// A context used in TestConnClosing() which seeks to test that code which calls
|
||||
// Err() to see if a connection is already being closed does not then proceed to
|
||||
// try to acquire the mutex, as this would lead to deadlock. When Err() is called
|
||||
// this context acquires the lock itself, in order to force a deadlock (and test
|
||||
// failure on timeout).
|
||||
type testConnClosingContext struct {
|
||||
parent context.Context
|
||||
mu *sync.Mutex
|
||||
}
|
||||
|
||||
func (c *testConnClosingContext) Deadline() (deadline time.Time, ok bool) {
|
||||
d, o := c.parent.Deadline()
|
||||
return d, o
|
||||
}
|
||||
func (c *testConnClosingContext) Done() <-chan struct{} {
|
||||
return c.parent.Done()
|
||||
}
|
||||
func (c *testConnClosingContext) Err() error {
|
||||
// Deliberately deadlock if anything grabs the lock after checking Err()
|
||||
c.mu.Lock()
|
||||
return errors.New("testConnClosingContext error")
|
||||
}
|
||||
func (c *testConnClosingContext) Value(key interface{}) interface{} {
|
||||
return c.parent.Value(key)
|
||||
}
|
||||
func (*testConnClosingContext) String() string {
|
||||
return "testConnClosingContext"
|
||||
}
|
||||
|
||||
func TestConnClosing(t *testing.T) {
|
||||
privateKey, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatalf("generating private key: %v", err)
|
||||
}
|
||||
|
||||
epCh := make(chan []string, 100)
|
||||
conn, err := NewConn(Options{
|
||||
Logf: t.Logf,
|
||||
PacketListener: nettype.Std{},
|
||||
EndpointsFunc: func(eps []string) {
|
||||
epCh <- eps
|
||||
},
|
||||
SimulatedNetwork: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("constructing magicsock: %v", err)
|
||||
}
|
||||
|
||||
derpMap, cleanup := runDERPAndStun(t, t.Logf, nettype.Std{}, netaddr.IPv4(127, 0, 3, 1))
|
||||
defer cleanup()
|
||||
|
||||
// The point of this test case is to exercise handling in derpWriteChanOfAddr() which
|
||||
// returns early if connCtx.Err() returns non-nil, to avoid a deadlock on conn.mu.
|
||||
// We swap in a context which always returns an error, and deliberately grabs the lock
|
||||
// to cause a deadlock if magicsock.go tries to acquire the lock after calling Err().
|
||||
closingCtx := testConnClosingContext{parent: conn.connCtx, mu: &conn.mu}
|
||||
conn.connCtx = &closingCtx
|
||||
conn.Start()
|
||||
|
||||
conn.SetDERPMap(derpMap)
|
||||
if err := conn.SetPrivateKey(privateKey); err != nil {
|
||||
t.Fatalf("setting private key in magicsock: %v", err)
|
||||
}
|
||||
|
||||
tun := tuntest.NewChannelTUN()
|
||||
tsTun := tstun.WrapTUN(t.Logf, tun.TUN())
|
||||
tsTun.SetFilter(filter.NewAllowAllForTest(t.Logf))
|
||||
|
||||
dev := device.NewDevice(tsTun, &device.DeviceOptions{
|
||||
Logger: &device.Logger{
|
||||
Debug: logger.StdLogger(t.Logf),
|
||||
Info: logger.StdLogger(t.Logf),
|
||||
Error: logger.StdLogger(t.Logf),
|
||||
},
|
||||
CreateEndpoint: conn.CreateEndpoint,
|
||||
CreateBind: conn.CreateBind,
|
||||
SkipBindUpdate: true,
|
||||
})
|
||||
|
||||
dev.Up()
|
||||
conn.WaitReady(t)
|
||||
|
||||
// We don't assert any failures within the test itself. If derpWriteChanOfAddr tries to
|
||||
// grab the lock it will deadlock, and conn.WaitReady(t) will call t.Fatal() after timeout.
|
||||
// (verified by deliberately breaking derpWriteChanOfAddr)
|
||||
}
|
||||
|
||||
// Exercise a code path in sendDiscoMessage if the connection has been closed.
|
||||
func TestConnClosed(t *testing.T) {
|
||||
mstun := &natlab.Machine{Name: "stun"}
|
||||
@@ -646,12 +563,15 @@ func TestConnClosed(t *testing.T) {
|
||||
stunIP: sif.V4(),
|
||||
}
|
||||
|
||||
derpMap, cleanup := runDERPAndStun(t, t.Logf, d.stun, d.stunIP)
|
||||
logf, closeLogf := logger.LogfCloser(t.Logf)
|
||||
defer closeLogf()
|
||||
|
||||
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
|
||||
defer cleanup()
|
||||
|
||||
ms1 := newMagicStack(t, logger.WithPrefix(t.Logf, "conn1: "), d.m1, derpMap)
|
||||
ms1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap, true)
|
||||
defer ms1.Close()
|
||||
ms2 := newMagicStack(t, logger.WithPrefix(t.Logf, "conn2: "), d.m2, derpMap)
|
||||
ms2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap, true)
|
||||
defer ms2.Close()
|
||||
|
||||
cleanup = meshStacks(t.Logf, []*magicStack{ms1, ms2})
|
||||
@@ -925,18 +845,20 @@ func testActiveDiscovery(t *testing.T, d *devices) {
|
||||
setT(t)
|
||||
|
||||
start := time.Now()
|
||||
logf := func(msg string, args ...interface{}) {
|
||||
wlogf := func(msg string, args ...interface{}) {
|
||||
t.Helper()
|
||||
msg = fmt.Sprintf("%s: %s", time.Since(start).Truncate(time.Microsecond), msg)
|
||||
tlogf(msg, args...)
|
||||
}
|
||||
logf, closeLogf := logger.LogfCloser(wlogf)
|
||||
defer closeLogf()
|
||||
|
||||
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
|
||||
defer cleanup()
|
||||
|
||||
m1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap)
|
||||
m1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap, true)
|
||||
defer m1.Close()
|
||||
m2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap)
|
||||
m2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap, true)
|
||||
defer m2.Close()
|
||||
|
||||
cleanup = meshStacks(logf, []*magicStack{m1, m2})
|
||||
@@ -983,14 +905,17 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
|
||||
// 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()
|
||||
|
||||
m1 := newMagicStack(t, logf, d.m1, derpMap)
|
||||
m1 := newMagicStack(t, logf, d.m1, derpMap, false)
|
||||
defer m1.Close()
|
||||
m2 := newMagicStack(t, logf, d.m2, derpMap)
|
||||
m2 := newMagicStack(t, logf, d.m2, derpMap, false)
|
||||
defer m2.Close()
|
||||
|
||||
addrs := []netaddr.IPPort{
|
||||
@@ -999,10 +924,10 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1067,7 +992,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
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)
|
||||
@@ -1144,10 +1069,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)
|
||||
}
|
||||
|
||||
@@ -1160,10 +1085,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
|
||||
@@ -1189,10 +1114,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
|
||||
@@ -1206,7 +1131,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")
|
||||
}
|
||||
@@ -1469,7 +1398,7 @@ func stringifyConfig(cfg wgcfg.Config) string {
|
||||
return string(j)
|
||||
}
|
||||
|
||||
func TestDiscoEndpointAlignment(t *testing.T) {
|
||||
func Test32bitAlignment(t *testing.T) {
|
||||
var de discoEndpoint
|
||||
off := unsafe.Offsetof(de.lastRecvUnixAtomic)
|
||||
if off%8 != 0 {
|
||||
@@ -1481,6 +1410,8 @@ func TestDiscoEndpointAlignment(t *testing.T) {
|
||||
if de.isFirstRecvActivityInAwhile() {
|
||||
t.Error("expected false on second call")
|
||||
}
|
||||
var c Conn
|
||||
atomic.AddInt64(&c.derpRecvCountAtomic, 1)
|
||||
}
|
||||
|
||||
func BenchmarkReceiveFrom(b *testing.B) {
|
||||
@@ -1491,6 +1422,7 @@ func BenchmarkReceiveFrom(b *testing.B) {
|
||||
EndpointsFunc: func(eps []string) {
|
||||
b.Logf("endpoints: %q", eps)
|
||||
},
|
||||
DisableLegacyNetworking: true,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
@@ -1503,6 +1435,21 @@ func BenchmarkReceiveFrom(b *testing.B) {
|
||||
}
|
||||
defer sendConn.Close()
|
||||
|
||||
// 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{
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
DiscoKey: discoKey,
|
||||
Endpoints: []string{sendConn.LocalAddr().String()},
|
||||
},
|
||||
},
|
||||
})
|
||||
conn.CreateEndpoint([32]byte{1: 1}, "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345")
|
||||
conn.addValidDiscoPathForTest(discoKey, netaddr.MustParseIPPort(sendConn.LocalAddr().String()))
|
||||
|
||||
var dstAddr net.Addr = conn.pconn4.LocalAddr()
|
||||
sendBuf := make([]byte, 1<<10)
|
||||
for i := range sendBuf {
|
||||
@@ -1514,13 +1461,12 @@ func BenchmarkReceiveFrom(b *testing.B) {
|
||||
if _, err := sendConn.WriteTo(sendBuf, dstAddr); err != nil {
|
||||
b.Fatalf("WriteTo: %v", err)
|
||||
}
|
||||
n, ep, addr, err := conn.ReceiveIPv4(buf)
|
||||
n, ep, err := conn.ReceiveIPv4(buf)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = n
|
||||
_ = ep
|
||||
_ = addr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
"gvisor.dev/gvisor/pkg/waiter"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -62,7 +63,46 @@ func Impl(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock.
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ipstack.AddAddress(nicID, ipv4.ProtocolNumber, tcpip.Address(net.ParseIP("100.96.188.101").To4()))
|
||||
e.AddNetworkMapCallback(func(nm *controlclient.NetworkMap) {
|
||||
oldIPs := make(map[tcpip.Address]bool)
|
||||
for _, ip := range ipstack.AllAddresses()[nicID] {
|
||||
oldIPs[ip.AddressWithPrefix.Address] = true
|
||||
}
|
||||
newIPs := make(map[tcpip.Address]bool)
|
||||
for _, ip := range nm.Addresses {
|
||||
newIPs[tcpip.Address(ip.IPNet().IP)] = true
|
||||
}
|
||||
|
||||
ipsToBeAdded := make(map[tcpip.Address]bool)
|
||||
for ip := range newIPs {
|
||||
if !oldIPs[ip] {
|
||||
ipsToBeAdded[ip] = true
|
||||
}
|
||||
}
|
||||
ipsToBeRemoved := make(map[tcpip.Address]bool)
|
||||
for ip := range oldIPs {
|
||||
if !newIPs[ip] {
|
||||
ipsToBeRemoved[ip] = true
|
||||
}
|
||||
}
|
||||
|
||||
for ip := range ipsToBeRemoved {
|
||||
err := ipstack.RemoveAddress(nicID, ip)
|
||||
if err != nil {
|
||||
logf("netstack: could not deregister IP %s: %v", ip, err)
|
||||
} else {
|
||||
logf("netstack: deregistered IP %s", ip)
|
||||
}
|
||||
}
|
||||
for ip := range ipsToBeAdded {
|
||||
err := ipstack.AddAddress(nicID, ipv4.ProtocolNumber, ip)
|
||||
if err != nil {
|
||||
logf("netstack: could not register IP %s: %v", ip, err)
|
||||
} else {
|
||||
logf("netstack: registered IP %s", ip)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Add 0.0.0.0/0 default route.
|
||||
subnet, _ := tcpip.NewSubnet(tcpip.Address(strings.Repeat("\x00", 4)), tcpip.AddressMask(strings.Repeat("\x00", 4)))
|
||||
|
||||
@@ -32,35 +32,47 @@ type pendingOpenFlow struct {
|
||||
timer *time.Timer // until giving up on the flow
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) removeFlow(f flowtrack.Tuple) (removed bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
of, ok := e.pendOpen[f]
|
||||
if !ok {
|
||||
// Not a tracked flow (likely already removed)
|
||||
return false
|
||||
}
|
||||
of.timer.Stop()
|
||||
delete(e.pendOpen, f)
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.TUN) (res filter.Response) {
|
||||
res = filter.Accept // always
|
||||
|
||||
if pp.IPProto == packet.TSMP {
|
||||
res = filter.DropSilently
|
||||
rh, ok := pp.AsTailscaleRejectedHeader()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if f := rh.Flow(); e.removeFlow(f) {
|
||||
e.logf("open-conn-track: flow %v %v > %v rejected due to %v", rh.Proto, rh.Src, rh.Dst, rh.Reason)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if pp.IPVersion == 0 ||
|
||||
pp.IPProto != packet.TCP ||
|
||||
pp.TCPFlags&(packet.TCPSyn|packet.TCPRst) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
flow := flowtrack.Tuple{Dst: pp.Src, Src: pp.Dst} // src/dst reversed
|
||||
// Either a SYN or a RST came back. Remove it in either case.
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
of, ok := e.pendOpen[flow]
|
||||
if !ok {
|
||||
// Not a tracked flow.
|
||||
return
|
||||
f := flowtrack.Tuple{Dst: pp.Src, Src: pp.Dst} // src/dst reversed
|
||||
removed := e.removeFlow(f)
|
||||
if removed && pp.TCPFlags&packet.TCPRst != 0 {
|
||||
e.logf("open-conn-track: flow TCP %v got RST by peer", f)
|
||||
}
|
||||
of.timer.Stop()
|
||||
delete(e.pendOpen, flow)
|
||||
|
||||
if pp.TCPFlags&packet.TCPRst != 0 {
|
||||
// TODO(bradfitz): have peer send a IP proto 99 "why"
|
||||
// packet first with details and log that instead
|
||||
// (e.g. ACL prohibited, shields up, etc).
|
||||
e.logf("open-conn-track: flow %v got RST by peer", flow)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.I
|
||||
return nil, fmt.Errorf("interfaceFromLUID: interface with LUID %v not found", luid)
|
||||
}
|
||||
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
||||
const mtu = 0
|
||||
luid := winipcfg.LUID(tun.LUID())
|
||||
iface, err := interfaceFromLUID(luid,
|
||||
@@ -251,6 +251,15 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send non-nil return errors to retErrc, to interupt our background
|
||||
// setPrivateNetwork goroutine.
|
||||
retErrc := make(chan error, 1)
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
retErrc <- retErr
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
// It takes a weirdly long time for Windows to notice the
|
||||
// new interface has come up. Poll periodically until it
|
||||
@@ -262,11 +271,18 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
log.Printf("setPrivateNetwork(try=%d): %v", i, err)
|
||||
} else {
|
||||
if found {
|
||||
if i > 0 {
|
||||
log.Printf("setPrivateNetwork(try=%d): success", i)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("setPrivateNetwork(try=%d): not found", i)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
case <-retErrc:
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Printf("setPrivateNetwork: adapter LUID %v not found after %d tries, giving up", luid, tries)
|
||||
}()
|
||||
|
||||
@@ -366,7 +366,9 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error {
|
||||
// address is already assigned to the interface, or if the addition
|
||||
// fails.
|
||||
func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
|
||||
|
||||
if !r.v6Available && addr.IP.Is6() {
|
||||
return nil
|
||||
}
|
||||
if err := r.cmd.run("ip", "addr", "add", addr.String(), "dev", r.tunname); err != nil {
|
||||
return fmt.Errorf("adding address %q to tunnel interface: %w", addr, err)
|
||||
}
|
||||
@@ -380,6 +382,9 @@ func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
|
||||
// the address is not assigned to the interface, or if the removal
|
||||
// fails.
|
||||
func (r *linuxRouter) delAddress(addr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && addr.IP.Is6() {
|
||||
return nil
|
||||
}
|
||||
if err := r.delLoopbackRule(addr.IP); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -437,6 +442,9 @@ func (r *linuxRouter) delLoopbackRule(addr netaddr.IP) error {
|
||||
// interface. Fails if the route already exists, or if adding the
|
||||
// route fails.
|
||||
func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && cidr.IP.Is6() {
|
||||
return nil
|
||||
}
|
||||
args := []string{
|
||||
"ip", "route", "add",
|
||||
normalizeCIDR(cidr),
|
||||
@@ -452,6 +460,9 @@ func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error {
|
||||
// interface. Fails if the route doesn't exist, or if removing the
|
||||
// route fails.
|
||||
func (r *linuxRouter) delRoute(cidr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && cidr.IP.Is6() {
|
||||
return nil
|
||||
}
|
||||
args := []string{
|
||||
"ip", "route", "del",
|
||||
normalizeCIDR(cidr),
|
||||
|
||||
@@ -201,12 +201,11 @@ func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, e
|
||||
break
|
||||
}
|
||||
}
|
||||
if !anyHasSuffix {
|
||||
return netaddr.IP{}, dns.RCodeRefused, nil
|
||||
}
|
||||
|
||||
addr, found := dnsMap.nameToIP[domain]
|
||||
if !found {
|
||||
if !anyHasSuffix {
|
||||
return netaddr.IP{}, dns.RCodeRefused, nil
|
||||
}
|
||||
return netaddr.IP{}, dns.RCodeNameError, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -218,8 +218,8 @@ func (t *TUN) poll() {
|
||||
func (t *TUN) filterOut(p *packet.Parsed) filter.Response {
|
||||
|
||||
if t.PreFilterOut != nil {
|
||||
if t.PreFilterOut(p, t) == filter.Drop {
|
||||
return filter.Drop
|
||||
if res := t.PreFilterOut(p, t); res.IsDrop() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,8 +234,8 @@ func (t *TUN) filterOut(p *packet.Parsed) filter.Response {
|
||||
}
|
||||
|
||||
if t.PostFilterOut != nil {
|
||||
if t.PostFilterOut(p, t) == filter.Drop {
|
||||
return filter.Drop
|
||||
if res := t.PostFilterOut(p, t); res.IsDrop() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,12 +264,12 @@ func (t *TUN) Read(buf []byte, offset int) (int, error) {
|
||||
return 0, io.EOF
|
||||
case err := <-t.errors:
|
||||
return 0, err
|
||||
case packet := <-t.outbound:
|
||||
n = copy(buf[offset:], packet)
|
||||
case pkt := <-t.outbound:
|
||||
n = copy(buf[offset:], pkt)
|
||||
// t.buffer has a fixed location in memory,
|
||||
// so this is the easiest way to tell when it has been consumed.
|
||||
// &packet[0] can be used because empty packets do not reach t.outbound.
|
||||
if &packet[0] == &t.buffer[PacketStartOffset] {
|
||||
// &pkt[0] can be used because empty packets do not reach t.outbound.
|
||||
if &pkt[0] == &t.buffer[PacketStartOffset] {
|
||||
t.bufferConsumed <- struct{}{}
|
||||
} else {
|
||||
// If the packet is not from t.buffer, then it is an injected packet.
|
||||
@@ -307,8 +307,8 @@ func (t *TUN) filterIn(buf []byte) filter.Response {
|
||||
p.Decode(buf)
|
||||
|
||||
if t.PreFilterIn != nil {
|
||||
if t.PreFilterIn(p, t) == filter.Drop {
|
||||
return filter.Drop
|
||||
if res := t.PreFilterIn(p, t); res.IsDrop() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,22 +319,50 @@ func (t *TUN) filterIn(buf []byte) filter.Response {
|
||||
}
|
||||
|
||||
if filt.RunIn(p, t.filterFlags) != filter.Accept {
|
||||
|
||||
// Tell them, via TSMP, we're dropping them due to the ACL.
|
||||
// Their host networking stack can translate this into ICMP
|
||||
// or whatnot as required. But notably, their GUI or tailscale CLI
|
||||
// can show them a rejection history with reasons.
|
||||
if p.IPVersion == 4 && p.IPProto == packet.TCP && p.TCPFlags&packet.TCPSyn != 0 {
|
||||
rj := packet.TailscaleRejectedHeader{
|
||||
IPSrc: p.Dst.IP,
|
||||
IPDst: p.Src.IP,
|
||||
Src: p.Src,
|
||||
Dst: p.Dst,
|
||||
Proto: p.IPProto,
|
||||
Reason: packet.RejectedDueToACLs,
|
||||
}
|
||||
if filt.ShieldsUp() {
|
||||
rj.Reason = packet.RejectedDueToShieldsUp
|
||||
}
|
||||
pkt := packet.Generate(rj, nil)
|
||||
t.InjectOutbound(pkt)
|
||||
|
||||
// TODO(bradfitz): also send a TCP RST, after the TSMP message.
|
||||
}
|
||||
|
||||
return filter.Drop
|
||||
}
|
||||
|
||||
if t.PostFilterIn != nil {
|
||||
if t.PostFilterIn(p, t) == filter.Drop {
|
||||
return filter.Drop
|
||||
if res := t.PostFilterIn(p, t); res.IsDrop() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
return filter.Accept
|
||||
}
|
||||
|
||||
// Write accepts an incoming packet. The packet begins at buf[offset:],
|
||||
// like wireguard-go/tun.Device.Write.
|
||||
func (t *TUN) Write(buf []byte, offset int) (int, error) {
|
||||
if !t.disableFilter {
|
||||
response := t.filterIn(buf[offset:])
|
||||
if response != filter.Accept {
|
||||
res := t.filterIn(buf[offset:])
|
||||
if res == filter.DropSilently {
|
||||
return len(buf), nil
|
||||
}
|
||||
if res != filter.Accept {
|
||||
return 0, ErrFiltered
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +341,22 @@ func TestAllocs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
ftun, tun := newFakeTUN(t.Logf, false)
|
||||
|
||||
data := udp4("1.2.3.4", "5.6.7.8", 98, 98)
|
||||
_, err := ftun.Write(data, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tun.Close()
|
||||
_, err = ftun.Write(data, 0)
|
||||
if err == nil {
|
||||
t.Error("Expected error from ftun.Write() after Close()")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWrite(b *testing.B) {
|
||||
ftun, tun := newFakeTUN(b.Logf, true)
|
||||
defer tun.Close()
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -24,7 +23,6 @@ import (
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
@@ -47,6 +45,8 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/tstun"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wglog"
|
||||
)
|
||||
|
||||
// minimalMTU is the MTU we set on tailscale's TUN
|
||||
@@ -84,6 +84,7 @@ const (
|
||||
|
||||
type userspaceEngine struct {
|
||||
logf logger.Logf
|
||||
wgLogger *wglog.Logger //a wireguard-go logging wrapper
|
||||
reqCh chan struct{}
|
||||
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
|
||||
timeNow func() time.Time
|
||||
@@ -110,16 +111,18 @@ type userspaceEngine struct {
|
||||
trimmedDisco map[tailcfg.DiscoKey]bool // set of disco keys of peers currently excluded from wireguard config
|
||||
sentActivityAt map[netaddr.IP]*int64 // value is atomic int64 of unixtime
|
||||
destIPActivityFuncs map[netaddr.IP]func()
|
||||
statusBufioReader *bufio.Reader // reusable for UAPI
|
||||
|
||||
mu sync.Mutex // guards following; see lock order comment below
|
||||
closing bool // Close was called (even if we're still closing)
|
||||
statusCallback StatusCallback
|
||||
linkChangeCallback func(major bool, newState *interfaces.State)
|
||||
peerSequence []wgkey.Key
|
||||
endpoints []string
|
||||
pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers
|
||||
linkState *interfaces.State
|
||||
pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go
|
||||
mu sync.Mutex // guards following; see lock order comment below
|
||||
closing bool // Close was called (even if we're still closing)
|
||||
statusCallback StatusCallback
|
||||
linkChangeCallback func(major bool, newState *interfaces.State)
|
||||
peerSequence []wgkey.Key
|
||||
endpoints []string
|
||||
pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers
|
||||
linkState *interfaces.State
|
||||
pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go
|
||||
networkMapCallbacks map[*someHandle]NetworkMapCallback
|
||||
|
||||
// Lock ordering: magicsock.Conn.mu, wgLock, then mu.
|
||||
}
|
||||
@@ -191,6 +194,7 @@ func NewUserspaceEngine(logf logger.Logf, tunname string, listenPort uint16) (En
|
||||
|
||||
e, err := NewUserspaceEngineAdvanced(conf)
|
||||
if err != nil {
|
||||
tun.Close()
|
||||
return nil, err
|
||||
}
|
||||
return e, err
|
||||
@@ -278,23 +282,9 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
e.tundev.PostFilterOut = e.trackOpenPostFilterOut
|
||||
}
|
||||
|
||||
// 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.HasPrefix(s, "Routine:")
|
||||
}
|
||||
filtered := logger.Filtered(logf, allowLogf)
|
||||
// flags==0 because logf is already nested in another logger.
|
||||
// The outer one can display the preferred log prefixes, etc.
|
||||
dlog := logger.StdLogger(filtered)
|
||||
logger := device.Logger{
|
||||
Debug: dlog,
|
||||
Info: dlog,
|
||||
Error: dlog,
|
||||
}
|
||||
|
||||
e.wgLogger = wglog.NewLogger(logf)
|
||||
opts := &device.DeviceOptions{
|
||||
Logger: &logger,
|
||||
Logger: e.wgLogger.DeviceLogger,
|
||||
HandshakeDone: func(peerKey device.NoisePublicKey, peer *device.Peer, deviceAllowedIPs *device.AllowedIPs) {
|
||||
// Send an unsolicited status event every time a
|
||||
// handshake completes. This makes sure our UI can
|
||||
@@ -773,6 +763,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
|
||||
full := e.lastCfgFull
|
||||
e.wgLogger.SetPeers(full.Peers)
|
||||
|
||||
// Compute a minimal config to pass to wireguard-go
|
||||
// based on the full config. Prune off all the peers
|
||||
@@ -806,11 +797,14 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
continue
|
||||
}
|
||||
tsIP := p.AllowedIPs[0].IP
|
||||
dk := discoKeyFromPeer(p)
|
||||
trackDisco = append(trackDisco, dk)
|
||||
trackIPs = append(trackIPs, tsIP)
|
||||
if e.isActiveSince(dk, tsIP, activeCutoff) {
|
||||
recentlyActive := false
|
||||
for _, cidr := range p.AllowedIPs {
|
||||
trackIPs = append(trackIPs, cidr.IP)
|
||||
recentlyActive = recentlyActive || e.isActiveSince(dk, cidr.IP, activeCutoff)
|
||||
}
|
||||
if recentlyActive {
|
||||
min.Peers = append(min.Peers, *p)
|
||||
if discoChanged[key.Public(p.PublicKey)] {
|
||||
needRemoveStep = true
|
||||
@@ -842,7 +836,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
if numRemove > 0 {
|
||||
e.logf("wgengine: Reconfig: removing session keys for %d peers", numRemove)
|
||||
if err := e.wgdev.Reconfig(&minner); err != nil {
|
||||
if err := wgcfg.ReconfigDevice(e.wgdev, &minner, e.logf); err != nil {
|
||||
e.logf("wgdev.Reconfig: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -850,7 +844,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
|
||||
e.logf("wgengine: Reconfig: configuring userspace wireguard config (with %d/%d peers)", len(min.Peers), len(full.Peers))
|
||||
if err := e.wgdev.Reconfig(&min); err != nil {
|
||||
if err := wgcfg.ReconfigDevice(e.wgdev, &min, e.logf); err != nil {
|
||||
e.logf("wgdev.Reconfig: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -1042,8 +1036,8 @@ func (e *userspaceEngine) getStatusCallback() StatusCallback {
|
||||
return e.statusCallback
|
||||
}
|
||||
|
||||
// TODO: this function returns an error but it's always nil, and when
|
||||
// there's actually a problem it just calls log.Fatal. Why?
|
||||
var singleNewline = []byte{'\n'}
|
||||
|
||||
func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
// Grab derpConns before acquiring wgLock to not violate lock ordering;
|
||||
// the DERPs method acquires magicsock.Conn.mu.
|
||||
@@ -1068,15 +1062,12 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// lineLen is the max UAPI line we expect. The longest I see is
|
||||
// len("preshared_key=")+64 hex+"\n" == 79. Add some slop.
|
||||
const lineLen = 100
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close() // to unblock writes on error path returns
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
bw := bufio.NewWriterSize(pw, lineLen)
|
||||
// TODO(apenwarr): get rid of silly uapi stuff for in-process comms
|
||||
// FIXME: get notified of status changes instead of polling.
|
||||
filter := device.IPCGetFilter{
|
||||
@@ -1084,23 +1075,34 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
// unused below; request that they not be sent instead.
|
||||
FilterAllowedIPs: true,
|
||||
}
|
||||
if err := e.wgdev.IpcGetOperationFiltered(bw, filter); err != nil {
|
||||
errc <- fmt.Errorf("IpcGetOperation: %w", err)
|
||||
return
|
||||
err := e.wgdev.IpcGetOperationFiltered(pw, filter)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("IpcGetOperation: %w", err)
|
||||
}
|
||||
errc <- bw.Flush()
|
||||
errc <- err
|
||||
}()
|
||||
|
||||
pp := make(map[wgkey.Key]*PeerStatus)
|
||||
p := &PeerStatus{}
|
||||
|
||||
var hst1, hst2, n int64
|
||||
var err error
|
||||
|
||||
bs := bufio.NewScanner(pr)
|
||||
bs.Buffer(make([]byte, lineLen), lineLen)
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
br := e.statusBufioReader
|
||||
if br != nil {
|
||||
br.Reset(pr)
|
||||
} else {
|
||||
br = bufio.NewReaderSize(pr, 1<<10)
|
||||
e.statusBufioReader = br
|
||||
}
|
||||
for {
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading from UAPI pipe: %w", err)
|
||||
}
|
||||
line = bytes.TrimSuffix(line, singleNewline)
|
||||
k := line
|
||||
var v mem.RO
|
||||
if i := bytes.IndexByte(line, '='); i != -1 {
|
||||
@@ -1111,7 +1113,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
case "public_key":
|
||||
pk, err := key.NewPublicFromHexMem(v)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: invalid key %#v", v)
|
||||
return nil, fmt.Errorf("IpcGetOperation: invalid key in line %q", line)
|
||||
}
|
||||
p = &PeerStatus{}
|
||||
pp[wgkey.Key(pk)] = p
|
||||
@@ -1122,34 +1124,31 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
n, err = mem.ParseInt(v, 10, 64)
|
||||
p.RxBytes = ByteCount(n)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: rx_bytes invalid: %#v", line)
|
||||
return nil, fmt.Errorf("IpcGetOperation: rx_bytes invalid: %#v", line)
|
||||
}
|
||||
case "tx_bytes":
|
||||
n, err = mem.ParseInt(v, 10, 64)
|
||||
p.TxBytes = ByteCount(n)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: tx_bytes invalid: %#v", line)
|
||||
return nil, fmt.Errorf("IpcGetOperation: tx_bytes invalid: %#v", line)
|
||||
}
|
||||
case "last_handshake_time_sec":
|
||||
hst1, err = mem.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: hst1 invalid: %#v", line)
|
||||
return nil, fmt.Errorf("IpcGetOperation: hst1 invalid: %#v", line)
|
||||
}
|
||||
case "last_handshake_time_nsec":
|
||||
hst2, err = mem.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: hst2 invalid: %#v", line)
|
||||
return nil, fmt.Errorf("IpcGetOperation: hst2 invalid: %#v", line)
|
||||
}
|
||||
if hst1 != 0 || hst2 != 0 {
|
||||
p.LastHandshake = time.Unix(hst1, hst2)
|
||||
} // else leave at time.IsZero()
|
||||
}
|
||||
}
|
||||
if err := bs.Err(); err != nil {
|
||||
log.Fatalf("reading IpcGetOperation output: %v", err)
|
||||
}
|
||||
if err := <-errc; err != nil {
|
||||
log.Fatalf("IpcGetOperation: %v", err)
|
||||
return nil, fmt.Errorf("IpcGetOperation: %v", err)
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
@@ -1290,6 +1289,21 @@ func (e *userspaceEngine) SetLinkChangeCallback(cb func(major bool, newState *in
|
||||
}
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) AddNetworkMapCallback(cb NetworkMapCallback) func() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.networkMapCallbacks == nil {
|
||||
e.networkMapCallbacks = make(map[*someHandle]NetworkMapCallback)
|
||||
}
|
||||
h := new(someHandle)
|
||||
e.networkMapCallbacks[h] = cb
|
||||
return func() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
delete(e.networkMapCallbacks, h)
|
||||
}
|
||||
}
|
||||
|
||||
func getLinkState() (*interfaces.State, error) {
|
||||
s, err := interfaces.GetState()
|
||||
if s != nil {
|
||||
@@ -1308,6 +1322,15 @@ func (e *userspaceEngine) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
|
||||
func (e *userspaceEngine) SetNetworkMap(nm *controlclient.NetworkMap) {
|
||||
e.magicConn.SetNetworkMap(nm)
|
||||
e.mu.Lock()
|
||||
callbacks := make([]NetworkMapCallback, 0, 4)
|
||||
for _, fn := range e.networkMapCallbacks {
|
||||
callbacks = append(callbacks, fn)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
for _, fn := range callbacks {
|
||||
fn(nm)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) DiscoPublicKey() tailcfg.DiscoKey {
|
||||
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tstun"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestNoteReceiveActivity(t *testing.T) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -21,6 +20,7 @@ import (
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
// NewWatchdog wraps an Engine and makes sure that all methods complete
|
||||
@@ -110,6 +110,11 @@ func (e *watchdogEngine) SetDERPMap(m *tailcfg.DERPMap) {
|
||||
func (e *watchdogEngine) SetNetworkMap(nm *controlclient.NetworkMap) {
|
||||
e.watchdog("SetNetworkMap", func() { e.wrap.SetNetworkMap(nm) })
|
||||
}
|
||||
func (e *watchdogEngine) AddNetworkMapCallback(callback NetworkMapCallback) func() {
|
||||
var fn func()
|
||||
e.watchdog("AddNetworkMapCallback", func() { fn = e.wrap.AddNetworkMapCallback(callback) })
|
||||
return func() { e.watchdog("RemoveNetworkMapCallback", fn) }
|
||||
}
|
||||
func (e *watchdogEngine) DiscoPublicKey() (k tailcfg.DiscoKey) {
|
||||
e.watchdog("DiscoPublicKey", func() { k = e.wrap.DiscoPublicKey() })
|
||||
return k
|
||||
|
||||
67
wgengine/wgcfg/config.go
Normal file
67
wgengine/wgcfg/config.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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 wgcfg has types and a parser for representing WireGuard config.
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Config is a WireGuard configuration.
|
||||
// It only supports the set of things Tailscale uses.
|
||||
type Config struct {
|
||||
Name string
|
||||
PrivateKey PrivateKey
|
||||
Addresses []netaddr.IPPrefix
|
||||
ListenPort uint16
|
||||
MTU uint16
|
||||
DNS []netaddr.IP
|
||||
Peers []Peer
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
PublicKey Key
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints string // comma-separated host/port pairs: "1.2.3.4:56,[::]:80"
|
||||
PersistentKeepalive uint16
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of Config.
|
||||
// The result aliases no memory with the original.
|
||||
func (cfg Config) Copy() Config {
|
||||
res := cfg
|
||||
if res.Addresses != nil {
|
||||
res.Addresses = append([]netaddr.IPPrefix{}, res.Addresses...)
|
||||
}
|
||||
if res.DNS != nil {
|
||||
res.DNS = append([]netaddr.IP{}, res.DNS...)
|
||||
}
|
||||
peers := make([]Peer, 0, len(res.Peers))
|
||||
for _, peer := range res.Peers {
|
||||
peers = append(peers, peer.Copy())
|
||||
}
|
||||
res.Peers = peers
|
||||
return res
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of Peer.
|
||||
// The result aliases no memory with the original.
|
||||
func (peer Peer) Copy() Peer {
|
||||
res := peer
|
||||
if res.AllowedIPs != nil {
|
||||
res.AllowedIPs = append([]netaddr.IPPrefix{}, res.AllowedIPs...)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// PeerWithKey returns the Peer with key k and reports whether it was found.
|
||||
func (config Config) PeerWithKey(k Key) (Peer, bool) {
|
||||
for _, p := range config.Peers {
|
||||
if p.PublicKey == k {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return Peer{}, false
|
||||
}
|
||||
61
wgengine/wgcfg/device.go
Normal file
61
wgengine/wgcfg/device.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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 wgcfg
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func DeviceConfig(d *device.Device) (*Config, error) {
|
||||
r, w := io.Pipe()
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
errc <- d.IpcGetOperation(w)
|
||||
w.Close()
|
||||
}()
|
||||
cfg, err := FromUAPI(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := <-errc; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(cfg.Peers, func(i, j int) bool {
|
||||
return cfg.Peers[i].PublicKey.LessThan(&cfg.Peers[j].PublicKey)
|
||||
})
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// ReconfigDevice replaces the existing device configuration with cfg.
|
||||
func ReconfigDevice(d *device.Device, cfg *Config, logf logger.Logf) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logf("wgcfg.Reconfig failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
prev, err := DeviceConfig(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, w := io.Pipe()
|
||||
errc := make(chan error)
|
||||
go func() {
|
||||
errc <- d.IpcSetOperation(r)
|
||||
}()
|
||||
|
||||
err = cfg.ToUAPI(w, prev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Close()
|
||||
return <-errc
|
||||
}
|
||||
242
wgengine/wgcfg/device_test.go
Normal file
242
wgengine/wgcfg/device_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// 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 wgcfg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestDeviceConfig(t *testing.T) {
|
||||
newPrivateKey := func() (Key, PrivateKey) {
|
||||
t.Helper()
|
||||
pk, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Key(pk.Public()), PrivateKey(pk)
|
||||
}
|
||||
k1, pk1 := newPrivateKey()
|
||||
ip1 := netaddr.MustParseIPPrefix("10.0.0.1/32")
|
||||
|
||||
k2, pk2 := newPrivateKey()
|
||||
ip2 := netaddr.MustParseIPPrefix("10.0.0.2/32")
|
||||
|
||||
k3, _ := newPrivateKey()
|
||||
ip3 := netaddr.MustParseIPPrefix("10.0.0.3/32")
|
||||
|
||||
cfg1 := &Config{
|
||||
PrivateKey: PrivateKey(pk1),
|
||||
Peers: []Peer{{
|
||||
PublicKey: k2,
|
||||
AllowedIPs: []netaddr.IPPrefix{ip2},
|
||||
}},
|
||||
}
|
||||
|
||||
cfg2 := &Config{
|
||||
PrivateKey: PrivateKey(pk2),
|
||||
Peers: []Peer{{
|
||||
PublicKey: k1,
|
||||
AllowedIPs: []netaddr.IPPrefix{ip1},
|
||||
PersistentKeepalive: 5,
|
||||
}},
|
||||
}
|
||||
|
||||
device1 := device.NewDevice(newNilTun(), &device.DeviceOptions{
|
||||
Logger: device.NewLogger(device.LogLevelError, "device1"),
|
||||
})
|
||||
device2 := device.NewDevice(newNilTun(), &device.DeviceOptions{
|
||||
Logger: device.NewLogger(device.LogLevelError, "device2"),
|
||||
})
|
||||
defer device1.Close()
|
||||
defer device2.Close()
|
||||
|
||||
cmp := func(t *testing.T, d *device.Device, want *Config) {
|
||||
t.Helper()
|
||||
got, err := DeviceConfig(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prev := new(Config)
|
||||
gotbuf := new(strings.Builder)
|
||||
err = got.ToUAPI(gotbuf, prev)
|
||||
gotStr := gotbuf.String()
|
||||
if err != nil {
|
||||
t.Errorf("got.ToUAPI(): error: %v", err)
|
||||
return
|
||||
}
|
||||
wantbuf := new(strings.Builder)
|
||||
err = want.ToUAPI(wantbuf, prev)
|
||||
wantStr := wantbuf.String()
|
||||
if err != nil {
|
||||
t.Errorf("want.ToUAPI(): error: %v", err)
|
||||
return
|
||||
}
|
||||
if gotStr != wantStr {
|
||||
buf := new(bytes.Buffer)
|
||||
w := bufio.NewWriter(buf)
|
||||
if err := d.IpcGetOperation(w); err != nil {
|
||||
t.Errorf("on error, could not IpcGetOperation: %v", err)
|
||||
}
|
||||
w.Flush()
|
||||
t.Errorf("cfg:\n%s\n---- want:\n%s\n---- uapi:\n%s", gotStr, wantStr, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("device1 config", func(t *testing.T) {
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
})
|
||||
|
||||
t.Run("device2 config", func(t *testing.T) {
|
||||
if err := ReconfigDevice(device2, cfg2, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device2, cfg2)
|
||||
})
|
||||
|
||||
// This is only to test that Config and Reconfig are properly synchronized.
|
||||
t.Run("device2 config/reconfig", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
ReconfigDevice(device2, cfg2, t.Logf)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
DeviceConfig(device2)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("device1 modify peer", func(t *testing.T) {
|
||||
cfg1.Peers[0].Endpoints = "1.2.3.4:12345"
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
})
|
||||
|
||||
t.Run("device1 replace endpoint", func(t *testing.T) {
|
||||
cfg1.Peers[0].Endpoints = "1.1.1.1:123"
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
})
|
||||
|
||||
t.Run("device1 add new peer", func(t *testing.T) {
|
||||
cfg1.Peers = append(cfg1.Peers, Peer{
|
||||
PublicKey: k3,
|
||||
AllowedIPs: []netaddr.IPPrefix{ip3},
|
||||
})
|
||||
sort.Slice(cfg1.Peers, func(i, j int) bool {
|
||||
return cfg1.Peers[i].PublicKey.LessThan(&cfg1.Peers[j].PublicKey)
|
||||
})
|
||||
|
||||
origCfg, err := DeviceConfig(device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
|
||||
newCfg, err := DeviceConfig(device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
peer0 := func(cfg *Config) Peer {
|
||||
p, ok := cfg.PeerWithKey(k2)
|
||||
if !ok {
|
||||
t.Helper()
|
||||
t.Fatal("failed to look up peer 2")
|
||||
}
|
||||
return p
|
||||
}
|
||||
peersEqual := func(p, q Peer) bool {
|
||||
return p.PublicKey == q.PublicKey && p.PersistentKeepalive == q.PersistentKeepalive &&
|
||||
p.Endpoints == q.Endpoints && cidrsEqual(p.AllowedIPs, q.AllowedIPs)
|
||||
}
|
||||
if !peersEqual(peer0(origCfg), peer0(newCfg)) {
|
||||
t.Error("reconfig modified old peer")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("device1 remove peer", func(t *testing.T) {
|
||||
removeKey := cfg1.Peers[len(cfg1.Peers)-1].PublicKey
|
||||
cfg1.Peers = cfg1.Peers[:len(cfg1.Peers)-1]
|
||||
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
|
||||
newCfg, err := DeviceConfig(device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, ok := newCfg.PeerWithKey(removeKey)
|
||||
if ok {
|
||||
t.Error("reconfig failed to remove peer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: replace with a loopback tunnel
|
||||
type nilTun struct {
|
||||
events chan tun.Event
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func newNilTun() tun.Device {
|
||||
return &nilTun{
|
||||
events: make(chan tun.Event),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *nilTun) File() *os.File { return nil }
|
||||
func (t *nilTun) Flush() error { return nil }
|
||||
func (t *nilTun) MTU() (int, error) { return 1420, nil }
|
||||
func (t *nilTun) Name() (string, error) { return "niltun", nil }
|
||||
func (t *nilTun) Events() chan tun.Event { return t.events }
|
||||
|
||||
func (t *nilTun) Read(data []byte, offset int) (int, error) {
|
||||
<-t.closed
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (t *nilTun) Write(data []byte, offset int) (int, error) {
|
||||
<-t.closed
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (t *nilTun) Close() error {
|
||||
close(t.events)
|
||||
close(t.closed)
|
||||
return nil
|
||||
}
|
||||
240
wgengine/wgcfg/key.go
Normal file
240
wgengine/wgcfg/key.go
Normal file
@@ -0,0 +1,240 @@
|
||||
// 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 wgcfg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const KeySize = 32
|
||||
|
||||
// Key is curve25519 key.
|
||||
// It is used by WireGuard to represent public and preshared keys.
|
||||
type Key [KeySize]byte
|
||||
|
||||
// NewPresharedKey generates a new random key.
|
||||
func NewPresharedKey() (*Key, error) {
|
||||
var k [KeySize]byte
|
||||
_, err := rand.Read(k[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return (*Key)(&k), nil
|
||||
}
|
||||
|
||||
func ParseKey(b64 string) (*Key, error) { return parseKeyBase64(base64.StdEncoding, b64) }
|
||||
|
||||
func ParseHexKey(s string) (Key, error) {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return Key{}, &ParseError{"invalid hex key: " + err.Error(), s}
|
||||
}
|
||||
if len(b) != KeySize {
|
||||
return Key{}, &ParseError{fmt.Sprintf("invalid hex key length: %d", len(b)), s}
|
||||
}
|
||||
|
||||
var key Key
|
||||
copy(key[:], b)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func ParsePrivateHexKey(v string) (PrivateKey, error) {
|
||||
k, err := ParseHexKey(v)
|
||||
if err != nil {
|
||||
return PrivateKey{}, err
|
||||
}
|
||||
pk := PrivateKey(k)
|
||||
if pk.IsZero() {
|
||||
// Do not clamp a zero key, pass the zero through
|
||||
// (much like NaN propagation) so that IsZero reports
|
||||
// a useful result.
|
||||
return pk, nil
|
||||
}
|
||||
pk.clamp()
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
func (k Key) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k Key) String() string { return k.ShortString() }
|
||||
func (k Key) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *Key) ShortString() string {
|
||||
long := k.Base64()
|
||||
return "[" + long[0:5] + "]"
|
||||
}
|
||||
|
||||
func (k *Key) IsZero() bool {
|
||||
if k == nil {
|
||||
return true
|
||||
}
|
||||
var zeros Key
|
||||
return subtle.ConstantTimeCompare(zeros[:], k[:]) == 1
|
||||
}
|
||||
|
||||
func (k *Key) MarshalJSON() ([]byte, error) {
|
||||
if k == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `"%x"`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *Key) UnmarshalJSON(b []byte) error {
|
||||
if k == nil {
|
||||
return errors.New("wgcfg.Key: UnmarshalJSON on nil pointer")
|
||||
}
|
||||
if len(b) < 3 || b[0] != '"' || b[len(b)-1] != '"' {
|
||||
return errors.New("wgcfg.Key: UnmarshalJSON not given a string")
|
||||
}
|
||||
b = b[1 : len(b)-1]
|
||||
key, err := ParseHexKey(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgcfg.Key: UnmarshalJSON: %v", err)
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Key) LessThan(b *Key) bool {
|
||||
for i := range a {
|
||||
if a[i] < b[i] {
|
||||
return true
|
||||
} else if a[i] > b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PrivateKey is curve25519 key.
|
||||
// It is used by WireGuard to represent private keys.
|
||||
type PrivateKey [KeySize]byte
|
||||
|
||||
// NewPrivateKey generates a new curve25519 secret key.
|
||||
// It conforms to the format described on https://cr.yp.to/ecdh.html.
|
||||
func NewPrivateKey() (PrivateKey, error) {
|
||||
k, err := NewPresharedKey()
|
||||
if err != nil {
|
||||
return PrivateKey{}, err
|
||||
}
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
return (PrivateKey)(*k), nil
|
||||
}
|
||||
|
||||
func ParsePrivateKey(b64 string) (*PrivateKey, error) {
|
||||
k, err := parseKeyBase64(base64.StdEncoding, b64)
|
||||
return (*PrivateKey)(k), err
|
||||
}
|
||||
|
||||
func (k *PrivateKey) String() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k *PrivateKey) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k *PrivateKey) Equal(k2 PrivateKey) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *PrivateKey) IsZero() bool {
|
||||
pk := Key(*k)
|
||||
return pk.IsZero()
|
||||
}
|
||||
|
||||
func (k *PrivateKey) clamp() {
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
}
|
||||
|
||||
// Public computes the public key matching this curve25519 secret key.
|
||||
func (k *PrivateKey) Public() Key {
|
||||
pk := Key(*k)
|
||||
if pk.IsZero() {
|
||||
panic("Tried to generate emptyPrivateKey.Public()")
|
||||
}
|
||||
var p [KeySize]byte
|
||||
curve25519.ScalarBaseMult(&p, (*[KeySize]byte)(k))
|
||||
return (Key)(p)
|
||||
}
|
||||
|
||||
func (k PrivateKey) MarshalText() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `privkey:%x`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *PrivateKey) UnmarshalText(b []byte) error {
|
||||
s := string(b)
|
||||
if !strings.HasPrefix(s, `privkey:`) {
|
||||
return errors.New("wgcfg.PrivateKey: UnmarshalText not given a private-key string")
|
||||
}
|
||||
s = strings.TrimPrefix(s, `privkey:`)
|
||||
key, err := ParseHexKey(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgcfg.PrivateKey: UnmarshalText: %v", err)
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k PrivateKey) SharedSecret(pub Key) (ss [KeySize]byte) {
|
||||
apk := (*[KeySize]byte)(&pub)
|
||||
ask := (*[KeySize]byte)(&k)
|
||||
curve25519.ScalarMult(&ss, ask, apk) //lint:ignore SA1019 Jason says this is OK; match wireguard-go exactyl
|
||||
return ss
|
||||
}
|
||||
|
||||
func parseKeyBase64(enc *base64.Encoding, s string) (*Key, error) {
|
||||
k, err := enc.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, &ParseError{"Invalid key: " + err.Error(), s}
|
||||
}
|
||||
if len(k) != KeySize {
|
||||
return nil, &ParseError{"Keys must decode to exactly 32 bytes", s}
|
||||
}
|
||||
var key Key
|
||||
copy(key[:], k)
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func ParseSymmetricKey(b64 string) (SymmetricKey, error) {
|
||||
k, err := parseKeyBase64(base64.StdEncoding, b64)
|
||||
if err != nil {
|
||||
return SymmetricKey{}, err
|
||||
}
|
||||
return SymmetricKey(*k), nil
|
||||
}
|
||||
|
||||
func ParseSymmetricHexKey(s string) (SymmetricKey, error) {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return SymmetricKey{}, &ParseError{"invalid symmetric hex key: " + err.Error(), s}
|
||||
}
|
||||
if len(b) != chacha20poly1305.KeySize {
|
||||
return SymmetricKey{}, &ParseError{fmt.Sprintf("invalid symmetric hex key length: %d", len(b)), s}
|
||||
}
|
||||
var key SymmetricKey
|
||||
copy(key[:], b)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// SymmetricKey is a chacha20poly1305 key.
|
||||
// It is used by WireGuard to represent pre-shared symmetric keys.
|
||||
type SymmetricKey [chacha20poly1305.KeySize]byte
|
||||
|
||||
func (k SymmetricKey) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k SymmetricKey) String() string { return "sym:" + k.Base64()[:8] }
|
||||
func (k SymmetricKey) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k SymmetricKey) IsZero() bool { return k.Equal(SymmetricKey{}) }
|
||||
func (k SymmetricKey) Equal(k2 SymmetricKey) bool {
|
||||
return subtle.ConstantTimeCompare(k[:], k2[:]) == 1
|
||||
}
|
||||
111
wgengine/wgcfg/key_test.go
Normal file
111
wgengine/wgcfg/key_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 wgcfg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeyBasics(t *testing.T) {
|
||||
k1, err := NewPresharedKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := k1.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("JSON round-trip", func(t *testing.T) {
|
||||
// should preserve the keys
|
||||
k2 := new(Key)
|
||||
if err := k2.UnmarshalJSON(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(k1[:], k2[:]) {
|
||||
t.Fatalf("k1 %v != k2 %v", k1[:], k2[:])
|
||||
}
|
||||
if b1, b2 := k1.String(), k2.String(); b1 != b2 {
|
||||
t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON incompatible with PrivateKey", func(t *testing.T) {
|
||||
k2 := new(PrivateKey)
|
||||
if err := k2.UnmarshalText(b); err == nil {
|
||||
t.Fatalf("successfully decoded key as private key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second key", func(t *testing.T) {
|
||||
// A second call to NewPresharedKey should make a new key.
|
||||
k3, err := NewPresharedKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(k1[:], k3[:]) {
|
||||
t.Fatalf("k1 %v == k3 %v", k1[:], k3[:])
|
||||
}
|
||||
// Check for obvious comparables to make sure we are not generating bad strings somewhere.
|
||||
if b1, b2 := k1.String(), k3.String(); b1 == b2 {
|
||||
t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
|
||||
}
|
||||
})
|
||||
}
|
||||
func TestPrivateKeyBasics(t *testing.T) {
|
||||
pri, err := NewPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := pri.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("JSON round-trip", func(t *testing.T) {
|
||||
// should preserve the keys
|
||||
pri2 := new(PrivateKey)
|
||||
if err := pri2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(pri[:], pri2[:]) {
|
||||
t.Fatalf("pri %v != pri2 %v", pri[:], pri2[:])
|
||||
}
|
||||
if b1, b2 := pri.String(), pri2.String(); b1 != b2 {
|
||||
t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
|
||||
}
|
||||
if pub1, pub2 := pri.Public().String(), pri2.Public().String(); pub1 != pub2 {
|
||||
t.Fatalf("base64-encoded public keys do not match: %s, %s", pub1, pub2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON incompatible with Key", func(t *testing.T) {
|
||||
k2 := new(Key)
|
||||
if err := k2.UnmarshalJSON(b); err == nil {
|
||||
t.Fatalf("successfully decoded private key as key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second key", func(t *testing.T) {
|
||||
// A second call to New should make a new key.
|
||||
pri3, err := NewPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(pri[:], pri3[:]) {
|
||||
t.Fatalf("pri %v == pri3 %v", pri[:], pri3[:])
|
||||
}
|
||||
// Check for obvious comparables to make sure we are not generating bad strings somewhere.
|
||||
if b1, b2 := pri.String(), pri3.String(); b1 == b2 {
|
||||
t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
|
||||
}
|
||||
if pub1, pub2 := pri.Public().String(), pri3.Public().String(); pub1 == pub2 {
|
||||
t.Fatalf("base64-encoded public keys match: %s, %s", pub1, pub2)
|
||||
}
|
||||
})
|
||||
}
|
||||
201
wgengine/wgcfg/parser.go
Normal file
201
wgengine/wgcfg/parser.go
Normal file
@@ -0,0 +1,201 @@
|
||||
// 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 wgcfg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
type ParseError struct {
|
||||
why string
|
||||
offender string
|
||||
}
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
return fmt.Sprintf("%s: %q", e.why, e.offender)
|
||||
}
|
||||
|
||||
func validateEndpoints(s string) error {
|
||||
if s == "" {
|
||||
// Otherwise strings.Split of the empty string produces [""].
|
||||
return nil
|
||||
}
|
||||
vals := strings.Split(s, ",")
|
||||
for _, val := range vals {
|
||||
_, _, err := parseEndpoint(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseEndpoint(s string) (host string, port uint16, err error) {
|
||||
i := strings.LastIndexByte(s, ':')
|
||||
if i < 0 {
|
||||
return "", 0, &ParseError{"Missing port from endpoint", s}
|
||||
}
|
||||
host, portStr := s[:i], s[i+1:]
|
||||
if len(host) < 1 {
|
||||
return "", 0, &ParseError{"Invalid endpoint host", host}
|
||||
}
|
||||
uport, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
hostColon := strings.IndexByte(host, ':')
|
||||
if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 {
|
||||
err := &ParseError{"Brackets must contain an IPv6 address", host}
|
||||
if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 {
|
||||
maybeV6 := net.ParseIP(host[1 : len(host)-1])
|
||||
if maybeV6 == nil || len(maybeV6) != net.IPv6len {
|
||||
return "", 0, err
|
||||
}
|
||||
} else {
|
||||
return "", 0, err
|
||||
}
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
return host, uint16(uport), nil
|
||||
}
|
||||
|
||||
func parseKeyHex(s string) (*Key, error) {
|
||||
k, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, &ParseError{"Invalid key: " + err.Error(), s}
|
||||
}
|
||||
if len(k) != KeySize {
|
||||
return nil, &ParseError{"Keys must decode to exactly 32 bytes", s}
|
||||
}
|
||||
var key Key
|
||||
copy(key[:], k)
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
// FromUAPI generates a Config from r.
|
||||
// r should be generated by calling device.IpcGetOperation;
|
||||
// it is not compatible with other uapi streams.
|
||||
func FromUAPI(r io.Reader) (*Config, error) {
|
||||
cfg := new(Config)
|
||||
var peer *Peer // current peer being operated on
|
||||
deviceConfig := true
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(line, "=")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("failed to parse line %q, found %d =-separated parts, want 2", line, len(parts))
|
||||
}
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
if key == "public_key" {
|
||||
if deviceConfig {
|
||||
deviceConfig = false
|
||||
}
|
||||
// Load/create the peer we are now configuring.
|
||||
var err error
|
||||
peer, err = cfg.handlePublicKeyLine(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
if deviceConfig {
|
||||
err = cfg.handleDeviceLine(key, value)
|
||||
} else {
|
||||
err = cfg.handlePeerLine(peer, key, value)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handleDeviceLine(key, value string) error {
|
||||
switch key {
|
||||
case "private_key":
|
||||
k, err := parseKeyHex(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// wireguard-go guarantees not to send zero value; private keys are already clamped.
|
||||
cfg.PrivateKey = PrivateKey(*k)
|
||||
case "listen_port":
|
||||
port, err := strconv.ParseUint(value, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse listen_port: %w", err)
|
||||
}
|
||||
cfg.ListenPort = uint16(port)
|
||||
case "fwmark":
|
||||
// ignore
|
||||
default:
|
||||
return fmt.Errorf("unexpected IpcGetOperation key: %v", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handlePublicKeyLine(value string) (*Peer, error) {
|
||||
k, err := parseKeyHex(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.Peers = append(cfg.Peers, Peer{})
|
||||
peer := &cfg.Peers[len(cfg.Peers)-1]
|
||||
peer.PublicKey = *k
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handlePeerLine(peer *Peer, key, value string) error {
|
||||
switch key {
|
||||
case "endpoint":
|
||||
err := validateEndpoints(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.Endpoints = value
|
||||
case "persistent_keepalive_interval":
|
||||
n, err := strconv.ParseUint(value, 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.PersistentKeepalive = uint16(n)
|
||||
case "allowed_ip":
|
||||
ipp, err := netaddr.ParseIPPrefix(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.AllowedIPs = append(peer.AllowedIPs, ipp)
|
||||
case "protocol_version":
|
||||
if value != "1" {
|
||||
return fmt.Errorf("invalid protocol version: %v", value)
|
||||
}
|
||||
case "preshared_key", "last_handshake_time_sec", "last_handshake_time_nsec", "tx_bytes", "rx_bytes":
|
||||
// ignore
|
||||
default:
|
||||
return fmt.Errorf("unexpected IpcGetOperation key: %v", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
73
wgengine/wgcfg/parser_test.go
Normal file
73
wgengine/wgcfg/parser_test.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 wgcfg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func noError(t *testing.T, err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
t.Errorf("Error at %s:%d: %#v", fn, line, err)
|
||||
return false
|
||||
}
|
||||
|
||||
func equal(t *testing.T, expected, actual interface{}) bool {
|
||||
if reflect.DeepEqual(expected, actual) {
|
||||
return true
|
||||
}
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
t.Errorf("Failed equals at %s:%d\nactual %#v\nexpected %#v", fn, line, actual, expected)
|
||||
return false
|
||||
}
|
||||
|
||||
func TestParseEndpoint(t *testing.T) {
|
||||
_, _, err := parseEndpoint("[192.168.42.0:]:51880")
|
||||
if err == nil {
|
||||
t.Error("Error was expected")
|
||||
}
|
||||
host, port, err := parseEndpoint("192.168.42.0:51880")
|
||||
if noError(t, err) {
|
||||
equal(t, "192.168.42.0", host)
|
||||
equal(t, uint16(51880), port)
|
||||
}
|
||||
host, port, err = parseEndpoint("test.wireguard.com:18981")
|
||||
if noError(t, err) {
|
||||
equal(t, "test.wireguard.com", host)
|
||||
equal(t, uint16(18981), port)
|
||||
}
|
||||
host, port, err = parseEndpoint("[2607:5300:60:6b0::c05f:543]:2468")
|
||||
if noError(t, err) {
|
||||
equal(t, "2607:5300:60:6b0::c05f:543", host)
|
||||
equal(t, uint16(2468), port)
|
||||
}
|
||||
_, _, err = parseEndpoint("[::::::invalid:18981")
|
||||
if err == nil {
|
||||
t.Error("Error was expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEndpoints(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want error
|
||||
}{
|
||||
{"", nil},
|
||||
{"1.2.3.4:5", nil},
|
||||
{"1.2.3.4:5,6.7.8.9:10", nil},
|
||||
{",", &ParseError{why: "Missing port from endpoint", offender: ""}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := validateEndpoints(tt.in)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q = %#v (%s); want %#v (%s)", tt.in, got, got, tt.want, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
141
wgengine/wgcfg/writer.go
Normal file
141
wgengine/wgcfg/writer.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// 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 wgcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// ToUAPI writes cfg in UAPI format to w.
|
||||
// Prev is the previous device Config.
|
||||
// Prev is required so that we can remove now-defunct peers
|
||||
// without having to remove and re-add all peers.
|
||||
func (cfg *Config) ToUAPI(w io.Writer, prev *Config) error {
|
||||
var stickyErr error
|
||||
set := func(key, value string) {
|
||||
if stickyErr != nil {
|
||||
return
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s=%s\n", key, value)
|
||||
if err != nil {
|
||||
stickyErr = err
|
||||
}
|
||||
}
|
||||
setUint16 := func(key string, value uint16) {
|
||||
set(key, strconv.FormatUint(uint64(value), 10))
|
||||
}
|
||||
setPeer := func(peer Peer) {
|
||||
set("public_key", peer.PublicKey.HexString())
|
||||
}
|
||||
|
||||
// Device config.
|
||||
if prev.PrivateKey != cfg.PrivateKey {
|
||||
set("private_key", cfg.PrivateKey.HexString())
|
||||
}
|
||||
if prev.ListenPort != cfg.ListenPort {
|
||||
setUint16("listen_port", cfg.ListenPort)
|
||||
}
|
||||
|
||||
old := make(map[Key]Peer)
|
||||
for _, p := range prev.Peers {
|
||||
old[p.PublicKey] = p
|
||||
}
|
||||
|
||||
// Add/configure all new peers.
|
||||
for _, p := range cfg.Peers {
|
||||
oldPeer := old[p.PublicKey]
|
||||
setPeer(p)
|
||||
set("protocol_version", "1")
|
||||
|
||||
if !endpointsEqual(oldPeer.Endpoints, p.Endpoints) {
|
||||
set("endpoint", p.Endpoints)
|
||||
}
|
||||
|
||||
// TODO: replace_allowed_ips is expensive.
|
||||
// If p.AllowedIPs is a strict superset of oldPeer.AllowedIPs,
|
||||
// then skip replace_allowed_ips and instead add only
|
||||
// the new ipps with allowed_ip.
|
||||
if !cidrsEqual(oldPeer.AllowedIPs, p.AllowedIPs) {
|
||||
set("replace_allowed_ips", "true")
|
||||
for _, ipp := range p.AllowedIPs {
|
||||
set("allowed_ip", ipp.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Set PersistentKeepalive after the peer is otherwise configured,
|
||||
// because it can trigger handshake packets.
|
||||
if oldPeer.PersistentKeepalive != p.PersistentKeepalive {
|
||||
setUint16("persistent_keepalive_interval", p.PersistentKeepalive)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove peers that were present but should no longer be.
|
||||
for _, p := range cfg.Peers {
|
||||
delete(old, p.PublicKey)
|
||||
}
|
||||
for _, p := range old {
|
||||
setPeer(p)
|
||||
set("remove", "true")
|
||||
}
|
||||
|
||||
if stickyErr != nil {
|
||||
stickyErr = fmt.Errorf("ToUAPI: %w", stickyErr)
|
||||
}
|
||||
return stickyErr
|
||||
}
|
||||
|
||||
func endpointsEqual(x, y string) bool {
|
||||
// Cheap comparisons.
|
||||
if x == y {
|
||||
return true
|
||||
}
|
||||
xs := strings.Split(x, ",")
|
||||
ys := strings.Split(y, ",")
|
||||
if len(xs) != len(ys) {
|
||||
return false
|
||||
}
|
||||
// Otherwise, see if they're the same, but out of order.
|
||||
sort.Strings(xs)
|
||||
sort.Strings(ys)
|
||||
x = strings.Join(xs, ",")
|
||||
y = strings.Join(ys, ",")
|
||||
return x == y
|
||||
}
|
||||
|
||||
func cidrsEqual(x, y []netaddr.IPPrefix) bool {
|
||||
// TODO: re-implement using netaddr.IPSet.Equal.
|
||||
if len(x) != len(y) {
|
||||
return false
|
||||
}
|
||||
// First see if they're equal in order, without allocating.
|
||||
exact := true
|
||||
for i := range x {
|
||||
if x[i] != y[i] {
|
||||
exact = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if exact {
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, see if they're the same, but out of order.
|
||||
m := make(map[netaddr.IPPrefix]bool)
|
||||
for _, v := range x {
|
||||
m[v] = true
|
||||
}
|
||||
for _, v := range y {
|
||||
if !m[v] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
// ByteCount is the number of bytes that have been sent or received.
|
||||
@@ -49,6 +49,15 @@ type StatusCallback func(*Status, error)
|
||||
// NetInfoCallback is the type used by Engine.SetNetInfoCallback.
|
||||
type NetInfoCallback func(*tailcfg.NetInfo)
|
||||
|
||||
// NetworkMapCallback is the type used by callbacks that hook
|
||||
// into network map updates.
|
||||
type NetworkMapCallback func(*controlclient.NetworkMap)
|
||||
|
||||
// someHandle is allocated so its pointer address acts as a unique
|
||||
// map key handle. (It needs to have non-zero size for Go to guarantee
|
||||
// the pointer is unique.)
|
||||
type someHandle struct{ _ byte }
|
||||
|
||||
// ErrNoChanges is returned by Engine.Reconfig if no changes were made.
|
||||
var ErrNoChanges = errors.New("no changes made to Engine config")
|
||||
|
||||
@@ -114,6 +123,12 @@ type Engine interface {
|
||||
// The network map should only be read from.
|
||||
SetNetworkMap(*controlclient.NetworkMap)
|
||||
|
||||
// AddNetworkMapCallback adds a function to a list of callbacks
|
||||
// that are called when the network map updates. It returns a
|
||||
// function that when called would remove the function from the
|
||||
// list of callbacks.
|
||||
AddNetworkMapCallback(NetworkMapCallback) (removeCallback func())
|
||||
|
||||
// SetNetInfoCallback sets the function to call when a
|
||||
// new NetInfo summary is available.
|
||||
SetNetInfoCallback(NetInfoCallback)
|
||||
|
||||
89
wgengine/wglog/wglog.go
Normal file
89
wgengine/wglog/wglog.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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 wglog contains logging helpers for wireguard-go.
|
||||
package wglog
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
// A Logger is a wireguard-go log wrapper that cleans up and rewrites log lines.
|
||||
// It can be modified at run time to adjust to new wireguard-go configurations.
|
||||
type Logger struct {
|
||||
DeviceLogger *device.Logger
|
||||
replacer atomic.Value // of *strings.Replacer
|
||||
}
|
||||
|
||||
// NewLogger creates a new logger for use with wireguard-go.
|
||||
// This logger silences repetitive/unhelpful noisy log lines
|
||||
// and rewrites peer keys from wireguard-go into Tailscale format.
|
||||
func NewLogger(logf logger.Logf) *Logger {
|
||||
ret := new(Logger)
|
||||
|
||||
wrapper := func(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if strings.Contains(msg, "Routine:") {
|
||||
// wireguard-go logs as it starts and stops routines.
|
||||
// Drop those; there are a lot of them, and they're just noise.
|
||||
return
|
||||
}
|
||||
r := ret.replacer.Load()
|
||||
if r == nil {
|
||||
// No replacements specified; log as originally planned.
|
||||
logf(format, args...)
|
||||
return
|
||||
}
|
||||
// Do the replacements.
|
||||
new := r.(*strings.Replacer).Replace(msg)
|
||||
if new == msg {
|
||||
// No replacements. Log as originally planned.
|
||||
logf(format, args...)
|
||||
return
|
||||
}
|
||||
// We made some replacements. Log the new version.
|
||||
// This changes the format string,
|
||||
// which is somewhat unfortunate as it impacts rate limiting,
|
||||
// but there's not much we can do about that.
|
||||
logf("%s", new)
|
||||
}
|
||||
std := logger.StdLogger(wrapper)
|
||||
ret.DeviceLogger = &device.Logger{
|
||||
Debug: std,
|
||||
Info: std,
|
||||
Error: std,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// SetPeers adjusts x to rewrite the peer public keys found in peers.
|
||||
// SetPeers is safe for concurrent use.
|
||||
func (x *Logger) SetPeers(peers []wgcfg.Peer) {
|
||||
// Construct a new peer public key log rewriter.
|
||||
var replace []string
|
||||
for _, peer := range peers {
|
||||
old := "peer(" + wireguardGoString(peer.PublicKey) + ")"
|
||||
new := peer.PublicKey.ShortString()
|
||||
replace = append(replace, old, new)
|
||||
}
|
||||
r := strings.NewReplacer(replace...)
|
||||
x.replacer.Store(r)
|
||||
}
|
||||
|
||||
// wireguardGoString prints p in the same format used by wireguard-go.
|
||||
func wireguardGoString(k wgcfg.Key) string {
|
||||
base64Key := base64.StdEncoding.EncodeToString(k[:])
|
||||
abbreviatedKey := "invalid"
|
||||
if len(base64Key) == 44 {
|
||||
abbreviatedKey = base64Key[0:4] + "…" + base64Key[39:43]
|
||||
}
|
||||
return abbreviatedKey
|
||||
}
|
||||
59
wgengine/wglog/wglog_test.go
Normal file
59
wgengine/wglog/wglog_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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 wglog_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wglog"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
omit bool
|
||||
}{
|
||||
{"hi", "hi", false},
|
||||
{"Routine: starting", "", true},
|
||||
{"peer(IMTB…r7lM) says it misses you", "[IMTBr] says it misses you", false},
|
||||
}
|
||||
|
||||
c := make(chan string, 1)
|
||||
logf := func(format string, args ...interface{}) {
|
||||
s := fmt.Sprintf(format, args...)
|
||||
select {
|
||||
case c <- s:
|
||||
default:
|
||||
t.Errorf("wrote %q, but shouldn't have", s)
|
||||
}
|
||||
}
|
||||
|
||||
x := wglog.NewLogger(logf)
|
||||
key, err := wgcfg.ParseHexKey("20c4c1ae54e1fd37cab6e9a532ca20646aff496796cc41d4519560e5e82bee53")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
x.SetPeers([]wgcfg.Peer{{PublicKey: key}})
|
||||
|
||||
for _, tt := range tests {
|
||||
if tt.omit {
|
||||
// Write a message ourselves into the channel.
|
||||
// Then if logf also attempts to write into the channel, it'll fail.
|
||||
c <- ""
|
||||
}
|
||||
x.DeviceLogger.Info.Println(tt.in)
|
||||
got := <-c
|
||||
if tt.omit {
|
||||
continue
|
||||
}
|
||||
tt.want += "\n"
|
||||
if got != tt.want {
|
||||
t.Errorf("Println(%q) = %q want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user