Compare commits
222 Commits
crawshaw/u
...
Xe/tailtls
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5386001a | ||
|
|
758c37b83d | ||
|
|
85184a58ed | ||
|
|
9fc4e876e3 | ||
|
|
8ec44d0d5f | ||
|
|
61d0435ed9 | ||
|
|
0653efb092 | ||
|
|
49a3fcae78 | ||
|
|
4a59a2781a | ||
|
|
d24ed3f68e | ||
|
|
b3d6704aa3 | ||
|
|
cf06f9df37 | ||
|
|
ec036b3561 | ||
|
|
7901289578 | ||
|
|
5a60781919 | ||
|
|
5b5f032c9a | ||
|
|
773af7292b | ||
|
|
9da22dac3d | ||
|
|
16870cb754 | ||
|
|
36b1df1241 | ||
|
|
41da7620af | ||
|
|
400ed799e6 | ||
|
|
9fa6cdf7bf | ||
|
|
24ea365d48 | ||
|
|
3b541c833e | ||
|
|
68917fdb5d | ||
|
|
945290cc3f | ||
|
|
57b039c51d | ||
|
|
c5d572f371 | ||
|
|
f7da8c77bd | ||
|
|
5b94f67956 | ||
|
|
a34350ffda | ||
|
|
d3acd35a90 | ||
|
|
a63c4ab378 | ||
|
|
4004b22fe5 | ||
|
|
293431aaea | ||
|
|
edb33d65c3 | ||
|
|
7e9e72887c | ||
|
|
cf90392174 | ||
|
|
0b392dbaf7 | ||
|
|
89a68a4c22 | ||
|
|
5e005a658f | ||
|
|
eabca699ec | ||
|
|
da7544bcc5 | ||
|
|
3e1daab704 | ||
|
|
d2ef73ed82 | ||
|
|
d6dde5a1ac | ||
|
|
eccc2ac6ee | ||
|
|
ad63fc0510 | ||
|
|
87137405e5 | ||
|
|
40e13c316c | ||
|
|
0edd2d1cd5 | ||
|
|
01bd789c26 | ||
|
|
b3abdc381d | ||
|
|
e6fbc0cd54 | ||
|
|
5f36ab8a90 | ||
|
|
2b082959db | ||
|
|
1ec99e99f4 | ||
|
|
12148dcf48 | ||
|
|
337757a819 | ||
|
|
0532eb30db | ||
|
|
f771327f0c | ||
|
|
649f7556e8 | ||
|
|
c7bff35fee | ||
|
|
6d82a18916 | ||
|
|
c467ed0b62 | ||
|
|
3fd5f4380f | ||
|
|
17b5782b3a | ||
|
|
7e6a1ef4f1 | ||
|
|
7e8d5ed6f3 | ||
|
|
c17250cee2 | ||
|
|
c3d7115e63 | ||
|
|
72ace0acba | ||
|
|
d6e7cec6a7 | ||
|
|
408b0923a6 | ||
|
|
ff1954cfd9 | ||
|
|
5dc5bd8d20 | ||
|
|
ff597e773e | ||
|
|
0303ec44c3 | ||
|
|
c18b9d58aa | ||
|
|
b02eb1d5c5 | ||
|
|
3a2b0fc36c | ||
|
|
8d14bc32d1 | ||
|
|
84c3a09a8d | ||
|
|
6422789ea0 | ||
|
|
0fcc88873b | ||
|
|
c0ae1d2563 | ||
|
|
418adae379 | ||
|
|
ff16e58d23 | ||
|
|
15d329b4fa | ||
|
|
27e83402a8 | ||
|
|
b43362852c | ||
|
|
eeb97fd89f | ||
|
|
ccd36cb5b1 | ||
|
|
743293d473 | ||
|
|
2486d7cb9b | ||
|
|
ef241f782e | ||
|
|
073a3ec416 | ||
|
|
cb87b7aa5b | ||
|
|
06dccea416 | ||
|
|
05cc2f510b | ||
|
|
05e55f4a0b | ||
|
|
55b6753c11 | ||
|
|
429632d32c | ||
|
|
c1d009b9e9 | ||
|
|
ebae0d95d0 | ||
|
|
ef14663934 | ||
|
|
94f6257fde | ||
|
|
1f06f77dcb | ||
|
|
37c150aee1 | ||
|
|
15376f975b | ||
|
|
19189d7018 | ||
|
|
c41fe182f0 | ||
|
|
4d38194c21 | ||
|
|
e03fda7ae6 | ||
|
|
7c40a5d440 | ||
|
|
ada8cd99af | ||
|
|
94fb42d4b2 | ||
|
|
1df865a580 | ||
|
|
c1d377078d | ||
|
|
4bb2c6980d | ||
|
|
640de1921f | ||
|
|
aad46bd9ff | ||
|
|
c9bf773312 | ||
|
|
d36c0d3566 | ||
|
|
6e5175373e | ||
|
|
96ad68c5d6 | ||
|
|
bab2d92c42 | ||
|
|
3164c7410e | ||
|
|
0c546a28ba | ||
|
|
5302e4be96 | ||
|
|
dc2fbf5877 | ||
|
|
7b87c04861 | ||
|
|
3ad11f6b8c | ||
|
|
31e4f60047 | ||
|
|
a9c78910bd | ||
|
|
a47158e14d | ||
|
|
bc89a796ec | ||
|
|
22dbaa0894 | ||
|
|
d381bc2b6c | ||
|
|
c23a378f63 | ||
|
|
e4d2ef2b67 | ||
|
|
cf8fcc1254 | ||
|
|
869999955d | ||
|
|
f27950e97f | ||
|
|
060ba86baa | ||
|
|
675f9cd199 | ||
|
|
4a65b07e34 | ||
|
|
5df7ac70d6 | ||
|
|
2ce5fc7b0a | ||
|
|
3b5ada1fd8 | ||
|
|
75de4e9cc2 | ||
|
|
b0b0a80318 | ||
|
|
eebe7afad7 | ||
|
|
81cabf48ec | ||
|
|
139a6c4c9c | ||
|
|
a320d70614 | ||
|
|
04d24d3a38 | ||
|
|
422ea4980f | ||
|
|
10745c099a | ||
|
|
85fa1b0d61 | ||
|
|
59a906df47 | ||
|
|
c1293b3858 | ||
|
|
505f844a43 | ||
|
|
0b62f26349 | ||
|
|
09e692e318 | ||
|
|
ed3fb197ad | ||
|
|
a8e2cceefd | ||
|
|
c209278a9b | ||
|
|
9b101bd6af | ||
|
|
c60806b557 | ||
|
|
9f954628e5 | ||
|
|
e25afc6656 | ||
|
|
8e3b8dbb50 | ||
|
|
6cb2705833 | ||
|
|
8efc306e4f | ||
|
|
9310713bfb | ||
|
|
0bf515e780 | ||
|
|
1b4e007425 | ||
|
|
7ce9c7ce84 | ||
|
|
118fe105f5 | ||
|
|
c30fa5903d | ||
|
|
3552d86525 | ||
|
|
eaa0aef934 | ||
|
|
b956139b0c | ||
|
|
7a243ae5b1 | ||
|
|
c6ea282b3f | ||
|
|
6425f497b1 | ||
|
|
11fdb14c53 | ||
|
|
e7eb46bced | ||
|
|
1c56643136 | ||
|
|
cb030a0bb4 | ||
|
|
53199738fb | ||
|
|
2aa5df7ac1 | ||
|
|
521b44e653 | ||
|
|
27799a1a96 | ||
|
|
a6d02dc122 | ||
|
|
c759fcc7d3 | ||
|
|
75a7779b42 | ||
|
|
9af27ba829 | ||
|
|
def650b3e8 | ||
|
|
f55c2bccf5 | ||
|
|
569f70abfd | ||
|
|
695df497ba | ||
|
|
04fd94acd6 | ||
|
|
151b4415ca | ||
|
|
d86081f353 | ||
|
|
e5779f019e | ||
|
|
36a07089ee | ||
|
|
3e80806804 | ||
|
|
82fa15fa3b | ||
|
|
7817ab6b20 | ||
|
|
2662a1c98c | ||
|
|
47ace13ac8 | ||
|
|
c6d3f622e9 | ||
|
|
e538d47bd5 | ||
|
|
4a3e2842d9 | ||
|
|
14f9c75293 | ||
|
|
ddf3394b40 | ||
|
|
77696579f5 | ||
|
|
7742caef0a | ||
|
|
2fa004a2a0 |
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -41,6 +41,7 @@ body:
|
||||
- Windows
|
||||
- iOS
|
||||
- Android
|
||||
- Synology
|
||||
- Other
|
||||
validations:
|
||||
required: false
|
||||
@@ -49,7 +50,7 @@ body:
|
||||
attributes:
|
||||
label: OS version
|
||||
description: What OS version are you using?
|
||||
placeholder: e.g., Debian 11.0, macOS Big Sur 11.6
|
||||
placeholder: e.g., Debian 11.0, macOS Big Sur 11.6, Synology DSM 7
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
|
||||
2
.github/workflows/go_generate.yml
vendored
2
.github/workflows/go_generate.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
then
|
||||
pkgs=$(go list ./... | grep -v dnsfallback)
|
||||
else
|
||||
pkgs=$(go list ./...)
|
||||
pkgs=$(go list ./... | grep -v dnsfallback)
|
||||
fi
|
||||
go generate $pkgs
|
||||
echo
|
||||
|
||||
9
.github/workflows/linux.yml
vendored
9
.github/workflows/linux.yml
vendored
@@ -28,6 +28,15 @@ jobs:
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Get QEMU
|
||||
run: |
|
||||
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something
|
||||
# to run Go binaries. 5.2.0 (Debian bullseye) empirically works, and
|
||||
# use this PPA which brings in a modern qemu.
|
||||
sudo add-apt-repository -y ppa:jacob/virtualisation
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install qemu-user
|
||||
|
||||
- name: Run tests on linux
|
||||
run: go test -bench=. -benchtime=1x ./...
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
#
|
||||
# To build the Dockerfile:
|
||||
#
|
||||
# $ docker build -t tailscale:tailscale .
|
||||
# $ docker build -t tailscale/tailscale .
|
||||
#
|
||||
# To run the tailscaled agent:
|
||||
#
|
||||
# $ docker run -d --name=tailscaled -v /var/lib:/var/lib -v /dev/net/tun:/dev/net/tun --network=host --privileged tailscale:tailscale tailscaled
|
||||
# $ docker run -d --name=tailscaled -v /var/lib:/var/lib -v /dev/net/tun:/dev/net/tun --network=host --privileged tailscale/tailscale tailscaled
|
||||
#
|
||||
# To then log in:
|
||||
#
|
||||
@@ -48,8 +48,9 @@ ARG VERSION_SHORT=""
|
||||
ENV VERSION_SHORT=$VERSION_SHORT
|
||||
ARG VERSION_GIT_HASH=""
|
||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN go install -tags=xversion -ldflags="\
|
||||
RUN GOARCH=$TARGETARCH go install -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" \
|
||||
|
||||
6
Makefile
6
Makefile
@@ -1,3 +1,5 @@
|
||||
IMAGE_REPO ?= tailscale/tailscale
|
||||
|
||||
usage:
|
||||
echo "See Makefile"
|
||||
|
||||
@@ -21,6 +23,10 @@ build386:
|
||||
buildlinuxarm:
|
||||
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
|
||||
buildmultiarchimage:
|
||||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t ${IMAGE_REPO}:latest --push -f Dockerfile .
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
|
||||
|
||||
staticcheck:
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.17.0
|
||||
1.19.0
|
||||
|
||||
@@ -56,7 +56,7 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, 41112)
|
||||
return safesocket.Connect(TailscaledSocket, safesocket.WindowsLocalPort)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -196,6 +196,12 @@ func Goroutines(ctx context.Context) ([]byte, error) {
|
||||
return get200(ctx, "/localapi/v0/goroutines")
|
||||
}
|
||||
|
||||
// DaemonMetrics returns the Tailscale daemon's metrics in
|
||||
// the Prometheus text exposition format.
|
||||
func DaemonMetrics(ctx context.Context) ([]byte, error) {
|
||||
return get200(ctx, "/localapi/v0/metrics")
|
||||
}
|
||||
|
||||
// Profile returns a pprof profile of the Tailscale daemon.
|
||||
func Profile(ctx context.Context, pprofType string, sec int) ([]byte, error) {
|
||||
var secArg string
|
||||
|
||||
@@ -95,7 +95,20 @@ func main() {
|
||||
}
|
||||
|
||||
contents := new(bytes.Buffer)
|
||||
fmt.Fprintf(contents, header, *flagTypes, pkg.Name)
|
||||
var flagArgs []string
|
||||
if *flagTypes != "" {
|
||||
flagArgs = append(flagArgs, "-type="+*flagTypes)
|
||||
}
|
||||
if *flagOutput != "" {
|
||||
flagArgs = append(flagArgs, "-output="+*flagOutput)
|
||||
}
|
||||
if *flagBuildTags != "" {
|
||||
flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
|
||||
}
|
||||
if *flagCloneFunc {
|
||||
flagArgs = append(flagArgs, "-clonefunc")
|
||||
}
|
||||
fmt.Fprintf(contents, header, strings.Join(flagArgs, " "), pkg.Name)
|
||||
fmt.Fprintf(contents, "import (\n")
|
||||
for s := range imports {
|
||||
fmt.Fprintf(contents, "\t%q\n", s)
|
||||
@@ -117,8 +130,8 @@ const header = `// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserve
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by the following command; DO NOT EDIT.
|
||||
// tailscale.com/cmd/cloner -type %s
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
//` + `go:generate` + ` go run tailscale.com/cmd/cloner %s
|
||||
|
||||
package %s
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -70,12 +69,12 @@ func init() {
|
||||
}
|
||||
|
||||
type config struct {
|
||||
PrivateKey wgkey.Private
|
||||
PrivateKey key.NodePrivate
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
if *dev {
|
||||
return config{PrivateKey: mustNewKey()}
|
||||
return config{PrivateKey: key.NewNode()}
|
||||
}
|
||||
if *configPath == "" {
|
||||
if os.Getuid() == 0 {
|
||||
@@ -101,21 +100,13 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewKey() wgkey.Private {
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func writeNewConfig() config {
|
||||
key := mustNewKey()
|
||||
k := key.NewNode()
|
||||
if err := os.MkdirAll(filepath.Dir(*configPath), 0777); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cfg := config{
|
||||
PrivateKey: key,
|
||||
PrivateKey: k,
|
||||
}
|
||||
b, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
@@ -150,9 +141,9 @@ func main() {
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
serveTLS := tsweb.IsProd443(*addr)
|
||||
serveTLS := tsweb.IsProd443(*addr) || *certMode == "manual"
|
||||
|
||||
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
||||
s := derp.NewServer(cfg.PrivateKey, log.Printf)
|
||||
s.SetVerifyClient(*verifyClients)
|
||||
|
||||
if *meshPSKFile != "" {
|
||||
@@ -173,7 +164,10 @@ func main() {
|
||||
expvar.Publish("derp", s.ExpVar())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/derp", derphttp.Handler(s))
|
||||
derpHandler := derphttp.Handler(s)
|
||||
derpHandler = addWebSocketSupport(s, derpHandler)
|
||||
mux.Handle("/derp", derpHandler)
|
||||
mux.HandleFunc("/derp/probe", probeHandler)
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -269,8 +263,18 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func serveSTUN(host string) {
|
||||
// probeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "HEAD", "GET":
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
default:
|
||||
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func serveSTUN(host string) {
|
||||
pc, err := net.ListenPacket("udp", net.JoinHostPort(host, "3478"))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open STUN listener: %v", err)
|
||||
|
||||
@@ -69,8 +69,8 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
|
||||
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
|
||||
add := func(k key.NodePublic) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.NodePublic) { s.RemovePacketForwarder(k, c) }
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
52
cmd/derper/websocket.go
Normal file
52
cmd/derper/websocket.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 main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"expvar"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/wsconn"
|
||||
)
|
||||
|
||||
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
||||
|
||||
// addWebSocketSupport returns a Handle wrapping base that adds WebSocket server support.
|
||||
func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
|
||||
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
|
||||
// speak WebSockets (they still assumed DERP's binary framining). So to distinguish
|
||||
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
|
||||
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
|
||||
base.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{"derp"},
|
||||
OriginPatterns: []string{"*"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("websocket.Accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer c.Close(websocket.StatusInternalError, "closing")
|
||||
if c.Subprotocol() != "derp" {
|
||||
c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
|
||||
return
|
||||
}
|
||||
counterWebSocketAccepts.Add(1)
|
||||
wc := wsconn.New(c)
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
|
||||
s.Accept(wc, brw, r.RemoteAddr)
|
||||
})
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.D
|
||||
}
|
||||
|
||||
func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*derphttp.Client, error) {
|
||||
priv := key.NewPrivate()
|
||||
priv := key.NewNode()
|
||||
dc := derphttp.NewRegionClient(priv, log.Printf, func() *tailcfg.DERPRegion {
|
||||
rid := n.RegionID
|
||||
return &tailcfg.DERPRegion{
|
||||
|
||||
@@ -7,7 +7,6 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
@@ -33,6 +32,6 @@ func runBugReport(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(logMarker)
|
||||
outln(logMarker)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ var certCmd = &ffcli.Command{
|
||||
ShortHelp: "get TLS certs",
|
||||
ShortUsage: "cert [flags] <domain>",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("cert", flag.ExitOnError)
|
||||
fs := newFlagSet("cert")
|
||||
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
|
||||
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
|
||||
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
|
||||
@@ -81,7 +81,7 @@ func runCert(ctx context.Context, args []string) error {
|
||||
domain := args[0]
|
||||
|
||||
printf := func(format string, a ...interface{}) {
|
||||
fmt.Printf(format, a...)
|
||||
printf(format, a...)
|
||||
}
|
||||
if certArgs.certFile == "-" || certArgs.keyFile == "-" {
|
||||
printf = log.Printf
|
||||
@@ -143,7 +143,7 @@ func runCert(ctx context.Context, args []string) error {
|
||||
|
||||
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
|
||||
if filename == "-" {
|
||||
os.Stdout.Write(contents)
|
||||
Stdout.Write(contents)
|
||||
return false, nil
|
||||
}
|
||||
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {
|
||||
|
||||
@@ -31,6 +31,22 @@ import (
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
var Stderr io.Writer = os.Stderr
|
||||
var Stdout io.Writer = os.Stdout
|
||||
|
||||
func printf(format string, a ...interface{}) {
|
||||
fmt.Fprintf(Stdout, format, a...)
|
||||
}
|
||||
|
||||
// outln is like fmt.Println in the common case, except when Stdout is
|
||||
// changed (as in js/wasm).
|
||||
//
|
||||
// It's not named println because that looks like the Go built-in
|
||||
// which goes to stderr and formats slightly differently.
|
||||
func outln(a ...interface{}) {
|
||||
fmt.Fprintln(Stdout, a...)
|
||||
}
|
||||
|
||||
// ActLikeCLI reports whether a GUI application should act like the
|
||||
// CLI based on os.Args, GOOS, the context the process is running in
|
||||
// (pty, parent PID), etc.
|
||||
@@ -77,6 +93,16 @@ func ActLikeCLI() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func newFlagSet(name string) *flag.FlagSet {
|
||||
onError := flag.ExitOnError
|
||||
if runtime.GOOS == "js" {
|
||||
onError = flag.ContinueOnError
|
||||
}
|
||||
fs := flag.NewFlagSet(name, onError)
|
||||
fs.SetOutput(Stderr)
|
||||
return fs
|
||||
}
|
||||
|
||||
// Run runs the CLI. The args do not include the binary name.
|
||||
func Run(args []string) error {
|
||||
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
|
||||
@@ -86,11 +112,11 @@ func Run(args []string) error {
|
||||
var warnOnce sync.Once
|
||||
tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
|
||||
warnOnce.Do(func() {
|
||||
fmt.Fprintf(os.Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
|
||||
fmt.Fprintf(Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
|
||||
})
|
||||
})
|
||||
|
||||
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
|
||||
rootfs := newFlagSet("tailscale")
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
@@ -131,23 +157,33 @@ change in the future.
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func fatalf(format string, a ...interface{}) {
|
||||
if Fatalf != nil {
|
||||
Fatalf(format, a...)
|
||||
return
|
||||
}
|
||||
log.SetFlags(0)
|
||||
log.Fatalf(format, a...)
|
||||
}
|
||||
|
||||
// Fatalf, if non-nil, is used instead of log.Fatalf.
|
||||
var Fatalf func(format string, a ...interface{})
|
||||
|
||||
var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
@@ -155,7 +191,7 @@ var rootArgs struct {
|
||||
var gotSignal syncs.AtomicBool
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
c, err := safesocket.Connect(rootArgs.socket, 41112)
|
||||
c, err := safesocket.Connect(rootArgs.socket, safesocket.WindowsLocalPort)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
|
||||
@@ -759,6 +759,18 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
wantErrSubtr: "can't change --login-server without --force-reauth",
|
||||
},
|
||||
{
|
||||
name: "change_tags",
|
||||
flags: []string{"--advertise-tags=tag:foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -14,7 +16,9 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
@@ -24,44 +28,81 @@ import (
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
LongHelp: `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
|
||||
fs.BoolVar(&debugArgs.ipn, "ipn", false, "If true, subscribe to IPN notifications")
|
||||
fs.BoolVar(&debugArgs.prefs, "prefs", false, "If true, dump active prefs")
|
||||
fs.BoolVar(&debugArgs.derpMap, "derp", false, "If true, dump DERP map")
|
||||
fs.BoolVar(&debugArgs.pretty, "pretty", false, "If true, pretty-print output (for --prefs)")
|
||||
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
|
||||
fs.BoolVar(&debugArgs.env, "env", false, "dump environment")
|
||||
fs.BoolVar(&debugArgs.localCreds, "local-creds", false, "print how to connect to local tailscaled")
|
||||
fs := newFlagSet("debug")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
|
||||
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
|
||||
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
|
||||
return fs
|
||||
})(),
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "derp-map",
|
||||
Exec: runDERPMap,
|
||||
ShortHelp: "print DERP map",
|
||||
},
|
||||
{
|
||||
Name: "daemon-goroutines",
|
||||
Exec: runDaemonGoroutines,
|
||||
ShortHelp: "print tailscaled's goroutines",
|
||||
},
|
||||
{
|
||||
Name: "metrics",
|
||||
Exec: runDaemonMetrics,
|
||||
ShortHelp: "print tailscaled's metrics",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("metrics")
|
||||
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "env",
|
||||
Exec: runEnv,
|
||||
ShortHelp: "print cmd/tailscale environment",
|
||||
},
|
||||
{
|
||||
Name: "local-creds",
|
||||
Exec: runLocalCreds,
|
||||
ShortHelp: "print how to access Tailscale local API",
|
||||
},
|
||||
{
|
||||
Name: "prefs",
|
||||
Exec: runPrefs,
|
||||
ShortHelp: "print prefs",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("prefs")
|
||||
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "watch-ipn",
|
||||
Exec: runWatchIPN,
|
||||
ShortHelp: "subscribe to IPN message bus",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("watch-ipn")
|
||||
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
env bool
|
||||
localCreds bool
|
||||
goroutines bool
|
||||
ipn bool
|
||||
netMap bool
|
||||
derpMap bool
|
||||
file string
|
||||
prefs bool
|
||||
pretty bool
|
||||
cpuSec int
|
||||
cpuFile string
|
||||
memFile string
|
||||
file string
|
||||
cpuSec int
|
||||
cpuFile string
|
||||
memFile string
|
||||
}
|
||||
|
||||
func writeProfile(dst string, v []byte) error {
|
||||
if dst == "-" {
|
||||
_, err := os.Stdout.Write(v)
|
||||
_, err := Stdout.Write(v)
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, v, 0600)
|
||||
@@ -81,26 +122,9 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if debugArgs.env {
|
||||
for _, e := range os.Environ() {
|
||||
fmt.Println(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if debugArgs.localCreds {
|
||||
port, token, err := safesocket.LocalTCPPortAndToken()
|
||||
if err == nil {
|
||||
fmt.Printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
|
||||
return nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Printf("curl http://localhost:41112/localapi/v0/status\n")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
return nil
|
||||
}
|
||||
var usedFlag bool
|
||||
if out := debugArgs.cpuFile; out != "" {
|
||||
usedFlag = true // TODO(bradfitz): add "profile" subcommand
|
||||
log.Printf("Capturing CPU profile for %v seconds ...", debugArgs.cpuSec)
|
||||
if v, err := tailscale.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
|
||||
return err
|
||||
@@ -112,6 +136,7 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
if out := debugArgs.memFile; out != "" {
|
||||
usedFlag = true // TODO(bradfitz): add "profile" subcommand
|
||||
log.Printf("Capturing memory profile ...")
|
||||
if v, err := tailscale.Profile(ctx, "heap", 0); err != nil {
|
||||
return err
|
||||
@@ -122,61 +147,14 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
log.Printf("Memory profile written to %s", outName(out))
|
||||
}
|
||||
}
|
||||
if debugArgs.prefs {
|
||||
prefs, err := tailscale.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debugArgs.pretty {
|
||||
fmt.Println(prefs.Pretty())
|
||||
} else {
|
||||
j, _ := json.MarshalIndent(prefs, "", "\t")
|
||||
fmt.Println(string(j))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if debugArgs.goroutines {
|
||||
goroutines, err := tailscale.Goroutines(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.Write(goroutines)
|
||||
return nil
|
||||
}
|
||||
if debugArgs.derpMap {
|
||||
dm, err := tailscale.CurrentDERPMap(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
|
||||
)
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(dm)
|
||||
return nil
|
||||
}
|
||||
if debugArgs.ipn {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if !debugArgs.netMap {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
fmt.Printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
return errors.New("exit")
|
||||
}
|
||||
if debugArgs.file != "" {
|
||||
usedFlag = true // TODO(bradfitz): add "file" subcommand
|
||||
if debugArgs.file == "get" {
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
fatalf("%v\n", err)
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e := json.NewEncoder(Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(wfs)
|
||||
return nil
|
||||
@@ -190,8 +168,151 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
log.Printf("Size: %v\n", size)
|
||||
io.Copy(os.Stdout, rc)
|
||||
io.Copy(Stdout, rc)
|
||||
return nil
|
||||
}
|
||||
if usedFlag {
|
||||
// TODO(bradfitz): delete this path when all debug flags are migrated
|
||||
// to subcommands.
|
||||
return nil
|
||||
}
|
||||
return errors.New("see 'tailscale debug --help")
|
||||
}
|
||||
|
||||
func runLocalCreds(ctx context.Context, args []string) error {
|
||||
port, token, err := safesocket.LocalTCPPortAndToken()
|
||||
if err == nil {
|
||||
printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
|
||||
return nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort)
|
||||
return nil
|
||||
}
|
||||
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
return nil
|
||||
}
|
||||
|
||||
var prefsArgs struct {
|
||||
pretty bool
|
||||
}
|
||||
|
||||
func runPrefs(ctx context.Context, args []string) error {
|
||||
prefs, err := tailscale.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prefsArgs.pretty {
|
||||
outln(prefs.Pretty())
|
||||
} else {
|
||||
j, _ := json.MarshalIndent(prefs, "", "\t")
|
||||
outln(string(j))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var watchIPNArgs struct {
|
||||
netmap bool
|
||||
}
|
||||
|
||||
func runWatchIPN(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if !watchIPNArgs.netmap {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
return errors.New("exit")
|
||||
}
|
||||
|
||||
func runDERPMap(ctx context.Context, args []string) error {
|
||||
dm, err := tailscale.CurrentDERPMap(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
|
||||
)
|
||||
}
|
||||
enc := json.NewEncoder(Stdout)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(dm)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runEnv(ctx context.Context, args []string) error {
|
||||
for _, e := range os.Environ() {
|
||||
outln(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDaemonGoroutines(ctx context.Context, args []string) error {
|
||||
goroutines, err := tailscale.Goroutines(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Stdout.Write(goroutines)
|
||||
return nil
|
||||
}
|
||||
|
||||
var metricsArgs struct {
|
||||
watch bool
|
||||
}
|
||||
|
||||
func runDaemonMetrics(ctx context.Context, args []string) error {
|
||||
last := map[string]int64{}
|
||||
for {
|
||||
out, err := tailscale.DaemonMetrics(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !metricsArgs.watch {
|
||||
Stdout.Write(out)
|
||||
return nil
|
||||
}
|
||||
bs := bufio.NewScanner(bytes.NewReader(out))
|
||||
type change struct {
|
||||
name string
|
||||
from, to int64
|
||||
}
|
||||
var changes []change
|
||||
var maxNameLen int
|
||||
for bs.Scan() {
|
||||
line := bytes.TrimSpace(bs.Bytes())
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
f := strings.Fields(string(line))
|
||||
if len(f) != 2 {
|
||||
continue
|
||||
}
|
||||
name := f[0]
|
||||
n, _ := strconv.ParseInt(f[1], 10, 64)
|
||||
prev, ok := last[name]
|
||||
if ok && prev == n {
|
||||
continue
|
||||
}
|
||||
last[name] = n
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
changes = append(changes, change{name, prev, n})
|
||||
if len(name) > maxNameLen {
|
||||
maxNameLen = len(name)
|
||||
}
|
||||
}
|
||||
if len(changes) > 0 {
|
||||
format := fmt.Sprintf("%%-%ds %%+5d => %%v\n", maxNameLen)
|
||||
for _, c := range changes {
|
||||
fmt.Fprintf(Stdout, format, c.name, c.to-c.from, c.to)
|
||||
}
|
||||
io.WriteString(Stdout, "\n")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
@@ -25,7 +23,7 @@ var downCmd = &ffcli.Command{
|
||||
|
||||
func runDown(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
@@ -33,7 +31,7 @@ func runDown(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("error fetching current status: %w", err)
|
||||
}
|
||||
if st.BackendState == "Stopped" {
|
||||
fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n")
|
||||
fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
|
||||
return nil
|
||||
}
|
||||
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
|
||||
@@ -55,7 +55,7 @@ var fileCpCmd = &ffcli.Command{
|
||||
ShortHelp: "Copy file(s) to a host",
|
||||
Exec: runCp,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("cp", flag.ExitOnError)
|
||||
fs := newFlagSet("cp")
|
||||
fs.StringVar(&cpArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
|
||||
fs.BoolVar(&cpArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&cpArgs.targets, "targets", false, "list possible file cp targets")
|
||||
@@ -101,7 +101,7 @@ func runCp(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("can't send to %s: %v", target, err)
|
||||
}
|
||||
if isOffline {
|
||||
fmt.Fprintf(os.Stderr, "# warning: %s is offline\n", target)
|
||||
fmt.Fprintf(Stderr, "# warning: %s is offline\n", target)
|
||||
}
|
||||
|
||||
if len(files) > 1 {
|
||||
@@ -172,7 +172,7 @@ func runCp(ctx context.Context, args []string) error {
|
||||
res.Body.Close()
|
||||
continue
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
io.Copy(Stdout, res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
@@ -293,7 +293,7 @@ func runCpTargets(ctx context.Context, args []string) error {
|
||||
if detail != "" {
|
||||
detail = "\t" + detail
|
||||
}
|
||||
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
|
||||
printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -304,7 +304,7 @@ var fileGetCmd = &ffcli.Command{
|
||||
ShortHelp: "Move files out of the Tailscale file inbox",
|
||||
Exec: runFileGet,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("get", flag.ExitOnError)
|
||||
fs := newFlagSet("get")
|
||||
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
|
||||
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
|
||||
return fs
|
||||
@@ -415,7 +415,7 @@ func waitForFile(ctx context.Context) error {
|
||||
fileWaiting := make(chan bool, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
fatalf("Notify.ErrMessage: %v\n", *n.ErrMessage)
|
||||
}
|
||||
if n.FilesWaiting != nil {
|
||||
select {
|
||||
|
||||
@@ -23,7 +23,7 @@ var ipCmd = &ffcli.Command{
|
||||
LongHelp: "Shows the Tailscale IP address of the current machine without an argument. With an argument, it shows the IP of a named peer.",
|
||||
Exec: runIP,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("ip", flag.ExitOnError)
|
||||
fs := newFlagSet("ip")
|
||||
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
|
||||
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
|
||||
return fs
|
||||
@@ -75,7 +75,7 @@ func runIP(ctx context.Context, args []string) error {
|
||||
for _, ip := range ips {
|
||||
if ip.Is4() && v4 || ip.Is6() && v6 {
|
||||
match = true
|
||||
fmt.Println(ip)
|
||||
outln(ip)
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
|
||||
@@ -6,7 +6,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
@@ -28,7 +28,7 @@ a reauthentication.
|
||||
|
||||
func runLogout(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
return tailscale.Logout(ctx)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ var netcheckCmd = &ffcli.Command{
|
||||
ShortHelp: "Print an analysis of local network conditions",
|
||||
Exec: runNetcheck,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("netcheck", flag.ExitOnError)
|
||||
fs := newFlagSet("netcheck")
|
||||
fs.StringVar(&netcheckArgs.format, "format", "", `output format; empty (for human-readable), "json" or "json-line"`)
|
||||
fs.DurationVar(&netcheckArgs.every, "every", 0, "if non-zero, do an incremental report with the given frequency")
|
||||
fs.BoolVar(&netcheckArgs.verbose, "verbose", false, "verbose logs")
|
||||
@@ -60,7 +60,7 @@ func runNetcheck(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(netcheckArgs.format, "json") {
|
||||
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
|
||||
fmt.Fprintln(Stderr, "# Warning: this JSON format is not yet considered a stable interface")
|
||||
}
|
||||
|
||||
dm, err := tailscale.CurrentDERPMap(ctx)
|
||||
@@ -82,7 +82,7 @@ func runNetcheck(ctx context.Context, args []string) error {
|
||||
c.Logf("GetReport took %v; err=%v", d.Round(time.Millisecond), err)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("netcheck: %v", err)
|
||||
return fmt.Errorf("netcheck: %w", err)
|
||||
}
|
||||
if err := printReport(dm, report); err != nil {
|
||||
return err
|
||||
@@ -112,36 +112,36 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
}
|
||||
if j != nil {
|
||||
j = append(j, '\n')
|
||||
os.Stdout.Write(j)
|
||||
Stdout.Write(j)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nReport:\n")
|
||||
fmt.Printf("\t* UDP: %v\n", report.UDP)
|
||||
printf("\nReport:\n")
|
||||
printf("\t* UDP: %v\n", report.UDP)
|
||||
if report.GlobalV4 != "" {
|
||||
fmt.Printf("\t* IPv4: yes, %v\n", report.GlobalV4)
|
||||
printf("\t* IPv4: yes, %v\n", report.GlobalV4)
|
||||
} else {
|
||||
fmt.Printf("\t* IPv4: (no addr found)\n")
|
||||
printf("\t* IPv4: (no addr found)\n")
|
||||
}
|
||||
if report.GlobalV6 != "" {
|
||||
fmt.Printf("\t* IPv6: yes, %v\n", report.GlobalV6)
|
||||
printf("\t* IPv6: yes, %v\n", report.GlobalV6)
|
||||
} else if report.IPv6 {
|
||||
fmt.Printf("\t* IPv6: (no addr found)\n")
|
||||
printf("\t* IPv6: (no addr found)\n")
|
||||
} else {
|
||||
fmt.Printf("\t* IPv6: no\n")
|
||||
printf("\t* IPv6: no\n")
|
||||
}
|
||||
fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
fmt.Printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
fmt.Printf("\t* PortMapping: %v\n", portMapping(report))
|
||||
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
printf("\t* PortMapping: %v\n", portMapping(report))
|
||||
|
||||
// When DERP latency checking failed,
|
||||
// magicsock will try to pick the DERP server that
|
||||
// most of your other nodes are also using
|
||||
if len(report.RegionLatency) == 0 {
|
||||
fmt.Printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
|
||||
printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
|
||||
} else {
|
||||
fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
|
||||
fmt.Printf("\t* DERP latency:\n")
|
||||
printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
|
||||
printf("\t* DERP latency:\n")
|
||||
var rids []int
|
||||
for rid := range dm.Regions {
|
||||
rids = append(rids, rid)
|
||||
@@ -168,7 +168,7 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
if netcheckArgs.verbose {
|
||||
derpNum = fmt.Sprintf("derp%d, ", rid)
|
||||
}
|
||||
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
|
||||
printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -45,7 +45,7 @@ relay node.
|
||||
`),
|
||||
Exec: runPing,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("ping", flag.ExitOnError)
|
||||
fs := newFlagSet("ping")
|
||||
fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
|
||||
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through IP + wireguard, but not involving host OS stack)")
|
||||
@@ -74,7 +74,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
fatalf("Notify.ErrMessage: %v", *n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
prc <- pr
|
||||
@@ -89,7 +89,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
if self {
|
||||
fmt.Printf("%v is local Tailscale IP\n", ip)
|
||||
printf("%v is local Tailscale IP\n", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -105,14 +105,14 @@ func runPing(ctx context.Context, args []string) error {
|
||||
timer := time.NewTimer(pingArgs.timeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
printf("timeout waiting for ping reply\n")
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
case pr := <-prc:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
if pr.IsLocalIP {
|
||||
fmt.Println(pr.Err)
|
||||
outln(pr.Err)
|
||||
return nil
|
||||
}
|
||||
return errors.New(pr.Err)
|
||||
@@ -132,7 +132,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if pr.PeerAPIPort != 0 {
|
||||
extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
|
||||
}
|
||||
fmt.Printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
|
||||
printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
|
||||
if pingArgs.tsmp {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ var statusCmd = &ffcli.Command{
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
Exec: runStatus,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("status", flag.ExitOnError)
|
||||
fs := newFlagSet("status")
|
||||
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||
@@ -70,7 +70,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s", j)
|
||||
printf("%s", j)
|
||||
return nil
|
||||
}
|
||||
if statusArgs.web {
|
||||
@@ -79,7 +79,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
statusURL := interfaces.HTTPOfListener(ln)
|
||||
fmt.Printf("Serving Tailscale status at %v ...\n", statusURL)
|
||||
printf("Serving Tailscale status at %v ...\n", statusURL)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
ln.Close()
|
||||
@@ -108,30 +108,30 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
|
||||
switch st.BackendState {
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
|
||||
fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState)
|
||||
os.Exit(1)
|
||||
case ipn.Stopped.String():
|
||||
fmt.Println("Tailscale is stopped.")
|
||||
outln("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
case ipn.NeedsLogin.String():
|
||||
fmt.Println("Logged out.")
|
||||
outln("Logged out.")
|
||||
if st.AuthURL != "" {
|
||||
fmt.Printf("\nLog in at: %s\n", st.AuthURL)
|
||||
printf("\nLog in at: %s\n", st.AuthURL)
|
||||
}
|
||||
os.Exit(1)
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
fmt.Println("Machine is not yet authorized by tailnet admin.")
|
||||
outln("Machine is not yet authorized by tailnet admin.")
|
||||
os.Exit(1)
|
||||
case ipn.Running.String(), ipn.Starting.String():
|
||||
// Run below.
|
||||
}
|
||||
|
||||
if len(st.Health) > 0 {
|
||||
fmt.Printf("# Health check:\n")
|
||||
printf("# Health check:\n")
|
||||
for _, m := range st.Health {
|
||||
fmt.Printf("# - %s\n", m)
|
||||
printf("# - %s\n", m)
|
||||
}
|
||||
fmt.Println()
|
||||
outln()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -190,7 +190,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
printPS(ps)
|
||||
}
|
||||
}
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
Stdout.Write(buf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ func effectiveGOOS() string {
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
upf := newFlagSet("up")
|
||||
|
||||
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
@@ -139,7 +139,7 @@ func (a upArgsT) getAuthKey() (string, error) {
|
||||
var upArgs upArgsT
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -277,7 +277,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
// It returns a non-nil justEditMP if we're already running and none of
|
||||
// the flags require a restart, so we can just do an EditPrefs call and
|
||||
// change the prefs at runtime (e.g. changing hostname, changing
|
||||
// advertised tags, routes, etc).
|
||||
// advertised routes, etc).
|
||||
//
|
||||
// It returns simpleUp if we're running a simple "tailscale up" to
|
||||
// transition to running from a previously-logged-in but down state,
|
||||
@@ -297,6 +297,8 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
|
||||
}
|
||||
|
||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
||||
|
||||
simpleUp = env.flagSet.NFlag() == 0 &&
|
||||
curPrefs.Persist != nil &&
|
||||
curPrefs.Persist.LoginName != "" &&
|
||||
@@ -306,7 +308,8 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
!env.upArgs.forceReauth &&
|
||||
!env.upArgs.reset &&
|
||||
env.upArgs.authKeyOrFile == "" &&
|
||||
!controlURLChanged
|
||||
!controlURLChanged &&
|
||||
!tagsChanged
|
||||
if justEdit {
|
||||
justEditMP = new(ipn.MaskedPrefs)
|
||||
justEditMP.WantRunningSet = true
|
||||
@@ -432,12 +435,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
case ipn.Running:
|
||||
// Done full authentication process
|
||||
if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(os.Stderr, "Success.\n")
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
}
|
||||
select {
|
||||
case running <- true:
|
||||
@@ -448,13 +451,13 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", q.ToString(false))
|
||||
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
@@ -20,7 +19,7 @@ var versionCmd = &ffcli.Command{
|
||||
ShortUsage: "version [flags]",
|
||||
ShortHelp: "Print Tailscale version",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("version", flag.ExitOnError)
|
||||
fs := newFlagSet("version")
|
||||
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
|
||||
return fs
|
||||
})(),
|
||||
@@ -33,19 +32,19 @@ var versionArgs struct {
|
||||
|
||||
func runVersion(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
if !versionArgs.daemon {
|
||||
fmt.Println(version.String())
|
||||
outln(version.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Client: %s\n", version.String())
|
||||
printf("Client: %s\n", version.String())
|
||||
|
||||
st, err := tailscale.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Daemon: %s\n", st.Version)
|
||||
printf("Daemon: %s\n", st.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ Tailscale, as opposed to a CLI or a native app.
|
||||
`),
|
||||
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
webf := flag.NewFlagSet("web", flag.ExitOnError)
|
||||
webf := newFlagSet("web")
|
||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||
return webf
|
||||
@@ -114,7 +114,7 @@ func tlsConfigFromEnvironment() *tls.Config {
|
||||
|
||||
func runWeb(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if webArgs.cgi {
|
||||
|
||||
@@ -4,6 +4,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
|
||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
@@ -23,6 +24,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
@@ -30,6 +35,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -66,7 +72,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
@@ -76,12 +82,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/derp+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
@@ -130,7 +136,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
embed from tailscale.com/cmd/tailscale/cli
|
||||
embed from tailscale.com/cmd/tailscale/cli+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
|
||||
@@ -193,8 +193,8 @@ func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
priv1 := key.NewNode()
|
||||
priv2 := key.NewNode()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
|
||||
@@ -59,7 +59,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
github.com/go-multierror/multierror from tailscale.com/cmd/tailscaled+
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
@@ -73,6 +72,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress from github.com/klauspost/compress/zstd
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
|
||||
@@ -91,24 +91,27 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/u-root/uio/ubinary from github.com/u-root/uio/uio
|
||||
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun
|
||||
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
|
||||
💣 golang.zx2c4.com/wireguard/device from tailscale.com/net/tstun+
|
||||
💣 golang.zx2c4.com/wireguard/ipc from golang.zx2c4.com/wireguard/device
|
||||
W 💣 golang.zx2c4.com/wireguard/ipc/winpipe from golang.zx2c4.com/wireguard/ipc
|
||||
W 💣 golang.zx2c4.com/wireguard/ipc/namedpipe from golang.zx2c4.com/wireguard/ipc
|
||||
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
|
||||
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device
|
||||
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/tun/wintun from golang.zx2c4.com/wireguard/tun
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
|
||||
inet.af/netaddr from inet.af/wf+
|
||||
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
|
||||
@@ -145,6 +148,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
@@ -153,6 +160,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
|
||||
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
@@ -167,14 +175,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/logtail from tailscale.com/logpolicy
|
||||
tailscale.com/logtail from tailscale.com/logpolicy+
|
||||
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
💣 tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/net/dns+
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
@@ -213,19 +221,21 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/clientmetric from tailscale.com/ipn/localapi+
|
||||
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
|
||||
tailscale.com/version from tailscale.com/client/tailscale+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
@@ -248,7 +258,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/derp+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
@@ -278,7 +288,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
container/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
@@ -27,17 +28,20 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/socks5/tssocks"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -78,6 +82,7 @@ var args struct {
|
||||
debug string
|
||||
port uint16
|
||||
statepath string
|
||||
statedir string
|
||||
socketpath string
|
||||
birdSocketPath string
|
||||
verbose int
|
||||
@@ -114,7 +119,8 @@ func main() {
|
||||
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM. If empty and --statedir is provided, the default is <statedir>/tailscaled.state")
|
||||
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
@@ -202,6 +208,16 @@ func trySynologyMigration(p string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func statePathOrDefault() string {
|
||||
if args.statepath != "" {
|
||||
return args.statepath
|
||||
}
|
||||
if args.statedir != "" {
|
||||
return filepath.Join(args.statedir, "tailscaled.state")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ipnServerOpts() (o ipnserver.Options) {
|
||||
// Allow changing the OS-specific IPN behavior for tests
|
||||
// so we can e.g. test Windows-specific behaviors on Linux.
|
||||
@@ -210,9 +226,15 @@ func ipnServerOpts() (o ipnserver.Options) {
|
||||
goos = runtime.GOOS
|
||||
}
|
||||
|
||||
o.Port = 41112
|
||||
o.StatePath = args.statepath
|
||||
o.SocketPath = args.socketpath // even for goos=="windows", for tests
|
||||
o.VarRoot = args.statedir
|
||||
|
||||
// If an absolute --state is provided but not --statedir, try to derive
|
||||
// a state directory.
|
||||
if o.VarRoot == "" && filepath.IsAbs(args.statepath) {
|
||||
if dir := filepath.Dir(args.statepath); strings.EqualFold(filepath.Base(dir), "tailscale") {
|
||||
o.VarRoot = dir
|
||||
}
|
||||
}
|
||||
|
||||
switch goos {
|
||||
default:
|
||||
@@ -227,7 +249,7 @@ func ipnServerOpts() (o ipnserver.Options) {
|
||||
func run() error {
|
||||
var err error
|
||||
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io")
|
||||
pol := logpolicy.New(logtail.CollectionNode)
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
defer func() {
|
||||
// Finish uploading logs after closing everything else.
|
||||
@@ -261,10 +283,10 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
if args.statepath == "" && args.statedir == "" {
|
||||
log.Fatalf("--statedir (or at least --state) is required")
|
||||
}
|
||||
if err := trySynologyMigration(args.statepath); err != nil {
|
||||
if err := trySynologyMigration(statePathOrDefault()); err != nil {
|
||||
log.Printf("error in synology migration: %v", err)
|
||||
}
|
||||
|
||||
@@ -289,10 +311,14 @@ func run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
var ns *netstack.Impl
|
||||
if useNetstack || wrapNetstack {
|
||||
onlySubnets := wrapNetstack && !useNetstack
|
||||
ns = mustStartNetstack(logf, e, onlySubnets)
|
||||
ns, err := newNetstack(logf, e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
@@ -332,8 +358,27 @@ func run() error {
|
||||
}()
|
||||
|
||||
opts := ipnServerOpts()
|
||||
opts.DebugMux = debugMux
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
|
||||
|
||||
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
|
||||
if err != nil {
|
||||
logf("ipnserver.New: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
}
|
||||
|
||||
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
err = srv.Run(ctx, ln)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && err != context.Canceled {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
@@ -357,7 +402,7 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, us
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil, false, multierror.New(errs)
|
||||
return nil, false, multierr.New(errs...)
|
||||
}
|
||||
|
||||
var wrapNetstack = shouldWrapNetstack()
|
||||
@@ -435,6 +480,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
|
||||
func newDebugMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/metrics", servePrometheusMetrics)
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
@@ -443,6 +489,11 @@ func newDebugMux() *http.ServeMux {
|
||||
return mux
|
||||
}
|
||||
|
||||
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
@@ -453,19 +504,12 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func mustStartNetstack(logf logger.Logf, e wgengine.Engine, onlySubnets bool) *netstack.Impl {
|
||||
func newNetstack(logf logger.Logf, e wgengine.Engine) (*netstack.Impl, error) {
|
||||
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
|
||||
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
|
||||
}
|
||||
ns, err := netstack.Create(logf, tunDev, e, magicConn, onlySubnets)
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
return ns
|
||||
return netstack.Create(logf, tunDev, e, magicConn)
|
||||
}
|
||||
|
||||
func mustStartTCPListener(name, addr string) net.Listener {
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
@@ -74,7 +75,11 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
args := []string{"/subproc", service.Policy.PublicID.String()}
|
||||
ipnserver.BabysitProc(ctx, args, log.Printf)
|
||||
// Make a logger without a date prefix, as filelogger
|
||||
// and logtail both already add their own. All we really want
|
||||
// from the log package is the automatic newline.
|
||||
logger := log.New(os.Stderr, "", 0)
|
||||
ipnserver.BabysitProc(ctx, args, logger.Printf)
|
||||
}()
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
|
||||
@@ -202,9 +207,14 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("engine: %w", err)
|
||||
}
|
||||
onlySubnets := true
|
||||
if wrapNetstack {
|
||||
mustStartNetstack(logf, eng, onlySubnets)
|
||||
ns, err := newNetstack(logf, eng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = false
|
||||
ns.ProcessSubnets = wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
return wgengine.NewWatchdog(eng), nil
|
||||
}
|
||||
@@ -266,7 +276,18 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
|
||||
}
|
||||
}
|
||||
err := ipnserver.Run(ctx, logf, logid, getEngine, ipnServerOpts())
|
||||
|
||||
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
err = ipnserver.Run(ctx, logf, ln, store, logid, getEngine, ipnServerOpts())
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
|
||||
86
cmd/tailtlsproxy/main.go
Normal file
86
cmd/tailtlsproxy/main.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
func envOr(name, def string) string {
|
||||
if val, ok := os.LookupEnv(name); ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
var (
|
||||
bind = flag.String("bind", ":443", "TCP hostport to bind to for TLS http traffic")
|
||||
hostname = flag.String("hostname", envOr("HOSTNAME", "toolname"), "hostname to register on tailnet (can be set with $HOSTNAME)")
|
||||
auth = flag.Bool("auth", false, "if set, authenticate with tailscale (enables verbose logging)")
|
||||
v = flag.Bool("v", false, "if set, enable verbose tailscale logs")
|
||||
to = flag.String("to", envOr("TO", "http://127.0.0.1:3030"), "HTTP/S url to reverse proxy to (can be set with $TO)")
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Setenv("TAILSCALE_USE_WIP_CODE", "true")
|
||||
flag.Parse()
|
||||
|
||||
srv := tsnet.Server{
|
||||
Hostname: *hostname,
|
||||
}
|
||||
|
||||
if *auth || *v {
|
||||
srv.Logf = log.Printf
|
||||
} else {
|
||||
srv.Logf = func(string, ...interface{}) {}
|
||||
}
|
||||
|
||||
if *auth {
|
||||
os.Setenv("TS_LOGIN", "1")
|
||||
}
|
||||
|
||||
ln, err := srv.Listen("tcp", *bind)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
u, err := url.Parse(*to)
|
||||
if err != nil {
|
||||
log.Fatalf("%s wasn't a valid URL: %v", *to, err)
|
||||
}
|
||||
|
||||
h := httputil.NewSingleHostReverseProxy(u)
|
||||
|
||||
ln = tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
c, err := tailscale.GetCertificate(chi)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
})
|
||||
|
||||
s := &http.Server{
|
||||
IdleTimeout: 5 * time.Minute,
|
||||
Addr: *bind,
|
||||
Handler: h,
|
||||
}
|
||||
|
||||
log.Printf("listening for https on https://%s.your.tailcert.domain and forwarding to %s", *hostname, *to)
|
||||
|
||||
log.Fatal(s.Serve(ln))
|
||||
}
|
||||
2
cmd/tailtlsproxy/tailtlsproxy-test.defaults
Normal file
2
cmd/tailtlsproxy/tailtlsproxy-test.defaults
Normal file
@@ -0,0 +1,2 @@
|
||||
HOSTNAME=test
|
||||
TO=http://127.0.0.1:3030
|
||||
19
cmd/tailtlsproxy/tailtlsproxy@.service
Normal file
19
cmd/tailtlsproxy/tailtlsproxy@.service
Normal file
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=Tailscale TLS Proxy bridge for %i
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Environment=HOME=/var/lib/private/tailtlsproxy-%i
|
||||
EnvironmentFile=/etc/default/tailtlsproxy-%i
|
||||
ExecStart=/usr/bin/tailtlsproxy
|
||||
Restart=on-failure
|
||||
RuntimeDirectory=tailtlsproxy-%i
|
||||
RuntimeDirectoryMode=0755
|
||||
StateDirectory=tailtlsproxy-%i
|
||||
StateDirectoryMode=0700
|
||||
CacheDirectory=tailtlsproxy-%i
|
||||
CacheDirectoryMode=0750
|
||||
DynamicUser=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
type LoginGoal struct {
|
||||
@@ -281,7 +281,6 @@ func (c *Auto) authRoutine() {
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("[v1] %s: %v", msg, err)
|
||||
err = fmt.Errorf("%s: %v", msg, err)
|
||||
// don't send status updates for context errors,
|
||||
// since context cancelation is always on purpose.
|
||||
if ctx.Err() == nil {
|
||||
@@ -431,7 +430,7 @@ func (c *Auto) mapRoutine() {
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("[v1] %s: %v", msg, err)
|
||||
err = fmt.Errorf("%s: %v", msg, err)
|
||||
err = fmt.Errorf("%s: %w", msg, err)
|
||||
// don't send status updates for context errors,
|
||||
// since context cancelation is always on purpose.
|
||||
if ctx.Err() == nil {
|
||||
@@ -599,9 +598,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
NetMap: nm,
|
||||
Hostinfo: hi,
|
||||
State: state,
|
||||
}
|
||||
if err != nil {
|
||||
new.Err = err.Error()
|
||||
Err: err,
|
||||
}
|
||||
if statusFunc != nil {
|
||||
statusFunc(new)
|
||||
@@ -702,7 +699,7 @@ func (c *Auto) Shutdown() {
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
// used exclusively in tests.
|
||||
func (c *Auto) TestOnlyNodePublicKey() wgkey.Key {
|
||||
func (c *Auto) TestOnlyNodePublicKey() key.NodePublic {
|
||||
priv := c.direct.GetPersist()
|
||||
return priv.PrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type LoginFlags int
|
||||
const (
|
||||
LoginDefault = LoginFlags(0)
|
||||
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
|
||||
LoginEphemeral // set RegisterRequest.Ephemeral
|
||||
)
|
||||
|
||||
// Client represents a client connection to the control server.
|
||||
@@ -78,3 +79,9 @@ type Client interface {
|
||||
// requesting a DNS record be created or updated.
|
||||
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
|
||||
}
|
||||
|
||||
// UserVisibleError is an error that should be shown to users.
|
||||
type UserVisibleError string
|
||||
|
||||
func (e UserVisibleError) Error() string { return string(e) }
|
||||
func (e UserVisibleError) UserVisibleError() string { return string(e) }
|
||||
|
||||
@@ -46,7 +46,7 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
@@ -61,7 +61,7 @@ type Direct struct {
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon // or nil
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
discoPubKey key.DiscoPublic
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
keepSharerAndUserSplit bool
|
||||
@@ -72,7 +72,7 @@ type Direct struct {
|
||||
serverKey key.MachinePublic
|
||||
persist persist.Persist
|
||||
authKey string
|
||||
tryingNewKey wgkey.Private
|
||||
tryingNewKey key.NodePrivate
|
||||
expiry *time.Time
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
@@ -89,7 +89,7 @@ type Options struct {
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
TimeNow func() time.Time // time.Now implementation used by Client
|
||||
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
|
||||
DiscoPublicKey tailcfg.DiscoKey
|
||||
DiscoPublicKey key.DiscoPublic
|
||||
NewDecompressor func() (Decompressor, error)
|
||||
KeepAlive bool
|
||||
Logf logger.Logf
|
||||
@@ -147,13 +147,20 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
}
|
||||
|
||||
httpc := opts.HTTPTestClient
|
||||
if httpc == nil && runtime.GOOS == "js" {
|
||||
// In js/wasm, net/http.Transport (as of Go 1.18) will
|
||||
// only use the browser's Fetch API if you're using
|
||||
// the DefaultClient (or a client without dial hooks
|
||||
// etc set).
|
||||
httpc = http.DefaultClient
|
||||
}
|
||||
if httpc == nil {
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
}
|
||||
dialer := netns.NewDialer()
|
||||
dialer := netns.NewDialer(opts.Logf)
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
@@ -285,8 +292,8 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
tryingNewKey := c.tryingNewKey
|
||||
serverKey := c.serverKey
|
||||
authKey := c.authKey
|
||||
hostinfo := c.hostinfo.Clone()
|
||||
backendLogID := hostinfo.BackendLogID
|
||||
hi := c.hostinfo.Clone()
|
||||
backendLogID := hi.BackendLogID
|
||||
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -327,7 +334,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
var oldNodeKey wgkey.Key
|
||||
var oldNodeKey key.NodePublic
|
||||
switch {
|
||||
case opt.Logout:
|
||||
tryingNewKey = persist.PrivateNodeKey
|
||||
@@ -336,12 +343,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
case regen || persist.PrivateNodeKey.IsZero():
|
||||
c.logf("Generating a new nodekey.")
|
||||
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
c.logf("login keygen: %v", err)
|
||||
return regen, opt.URL, err
|
||||
}
|
||||
tryingNewKey = key
|
||||
tryingNewKey = key.NewNode()
|
||||
default:
|
||||
// Try refreshing the current key first
|
||||
tryingNewKey = persist.PrivateNodeKey
|
||||
@@ -363,11 +365,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
now := time.Now().Round(time.Second)
|
||||
request := tailcfg.RegisterRequest{
|
||||
Version: 1,
|
||||
OldNodeKey: tailcfg.NodeKey(oldNodeKey),
|
||||
NodeKey: tailcfg.NodeKey(tryingNewKey.Public()),
|
||||
Hostinfo: hostinfo,
|
||||
OldNodeKey: oldNodeKey,
|
||||
NodeKey: tryingNewKey.Public(),
|
||||
Hostinfo: hi,
|
||||
Followup: opt.URL,
|
||||
Timestamp: &now,
|
||||
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
|
||||
}
|
||||
if opt.Logout {
|
||||
request.Expiry = time.Unix(123, 0) // far in the past
|
||||
@@ -435,6 +438,9 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
|
||||
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
|
||||
|
||||
if resp.Error != "" {
|
||||
return false, "", UserVisibleError(resp.Error)
|
||||
}
|
||||
if resp.NodeKeyExpired {
|
||||
if regen {
|
||||
return true, "", fmt.Errorf("weird: regen=true but server says NodeKeyExpired: %v", request.NodeKey)
|
||||
@@ -553,12 +559,21 @@ const pollTimeout = 120 * time.Second
|
||||
|
||||
// cb nil means to omit peers.
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
metricMapRequests.Add(1)
|
||||
metricMapRequestsActive.Add(1)
|
||||
defer metricMapRequestsActive.Add(-1)
|
||||
if maxPolls == -1 {
|
||||
metricMapRequestsPoll.Add(1)
|
||||
} else {
|
||||
metricMapRequestsLite.Add(1)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
serverKey := c.serverKey
|
||||
hostinfo := c.hostinfo.Clone()
|
||||
backendLogID := hostinfo.BackendLogID
|
||||
hi := c.hostinfo.Clone()
|
||||
backendLogID := hi.BackendLogID
|
||||
localPort := c.localPort
|
||||
var epStrs []string
|
||||
var epTypes []tailcfg.EndpointType
|
||||
@@ -597,18 +612,18 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
request := &tailcfg.MapRequest{
|
||||
Version: tailcfg.CurrentMapRequestVersion,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
NodeKey: persist.PrivateNodeKey.Public(),
|
||||
DiscoKey: c.discoPubKey,
|
||||
Endpoints: epStrs,
|
||||
EndpointTypes: epTypes,
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
Hostinfo: hi,
|
||||
DebugFlags: c.debugFlags,
|
||||
OmitPeers: cb == nil,
|
||||
}
|
||||
var extraDebugFlags []string
|
||||
if hostinfo != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
|
||||
ipForwardingBroken(hostinfo.RoutableIPs, c.linkMon.InterfaceState()) {
|
||||
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
|
||||
ipForwardingBroken(hi.RoutableIPs, c.linkMon.InterfaceState()) {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
|
||||
}
|
||||
if health.RouterHealth() != nil {
|
||||
@@ -617,6 +632,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
if health.NetworkCategoryHealth() != nil {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-network-category-unhealthy")
|
||||
}
|
||||
if hostinfo.DisabledEtcAptSource() {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-etc-apt-source-disabled")
|
||||
}
|
||||
if len(extraDebugFlags) > 0 {
|
||||
old := request.DebugFlags
|
||||
request.DebugFlags = append(old[:len(old):len(old)], extraDebugFlags...)
|
||||
@@ -739,11 +757,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
return err
|
||||
}
|
||||
|
||||
metricMapResponseMessages.Add(1)
|
||||
|
||||
if allowStream {
|
||||
health.GotStreamedMapResponse()
|
||||
}
|
||||
|
||||
if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) {
|
||||
metricMapResponsePings.Add(1)
|
||||
go answerPing(c.logf, c.httpc, pr)
|
||||
}
|
||||
|
||||
@@ -760,13 +781,23 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
return ctx.Err()
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
metricMapResponseKeepAlives.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
metricMapResponseMap.Add(1)
|
||||
if i > 0 {
|
||||
metricMapResponseMapDelta.Add(1)
|
||||
}
|
||||
|
||||
hasDebug := resp.Debug != nil
|
||||
// being conservative here, if Debug not present set to False
|
||||
controlknobs.SetDisableUPnP(hasDebug && resp.Debug.DisableUPnP.EqualBool(true))
|
||||
if hasDebug {
|
||||
if code := resp.Debug.Exit; code != nil {
|
||||
c.logf("exiting process with status %v per controlplane", *code)
|
||||
os.Exit(*code)
|
||||
}
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
@@ -1169,7 +1200,13 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<-
|
||||
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) {
|
||||
metricSetDNS.Add(1)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
metricSetDNSError.Add(1)
|
||||
}
|
||||
}()
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
c.mu.Unlock()
|
||||
@@ -1269,3 +1306,20 @@ func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")
|
||||
|
||||
metricMapRequests = clientmetric.NewCounter("controlclient_map_requests")
|
||||
metricMapRequestsLite = clientmetric.NewCounter("controlclient_map_requests_lite")
|
||||
metricMapRequestsPoll = clientmetric.NewCounter("controlclient_map_requests_poll")
|
||||
|
||||
metricMapResponseMessages = clientmetric.NewCounter("controlclient_map_response_message") // any message type
|
||||
metricMapResponsePings = clientmetric.NewCounter("controlclient_map_response_ping")
|
||||
metricMapResponseKeepAlives = clientmetric.NewCounter("controlclient_map_response_keepalive")
|
||||
metricMapResponseMap = clientmetric.NewCounter("controlclient_map_response_map") // any non-keepalive map response
|
||||
metricMapResponseMapDelta = clientmetric.NewCounter("controlclient_map_response_map_delta") // 2nd+ non-keepalive map response
|
||||
|
||||
metricSetDNS = clientmetric.NewCounter("controlclient_setdns")
|
||||
metricSetDNSError = clientmetric.NewCounter("controlclient_setdns_error")
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -29,7 +28,7 @@ import (
|
||||
// one MapRequest).
|
||||
type mapSession struct {
|
||||
// Immutable fields.
|
||||
privateNodeKey wgkey.Private
|
||||
privateNodeKey key.NodePrivate
|
||||
logf logger.Logf
|
||||
vlogf logger.Logf
|
||||
machinePubKey key.MachinePublic
|
||||
@@ -51,7 +50,7 @@ type mapSession struct {
|
||||
netMapBuilding *netmap.NetworkMap
|
||||
}
|
||||
|
||||
func newMapSession(privateNodeKey wgkey.Private) *mapSession {
|
||||
func newMapSession(privateNodeKey key.NodePrivate) *mapSession {
|
||||
ms := &mapSession{
|
||||
privateNodeKey: privateNodeKey,
|
||||
logf: logger.Discard,
|
||||
@@ -111,7 +110,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(ms.privateNodeKey.Public()),
|
||||
NodeKey: ms.privateNodeKey.Public(),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
@@ -170,11 +170,7 @@ func formatNodes(nodes []*tailcfg.Node) string {
|
||||
}
|
||||
|
||||
func newTestMapSession(t *testing.T) *mapSession {
|
||||
k, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return newMapSession(k)
|
||||
return newMapSession(key.NewNode())
|
||||
}
|
||||
|
||||
func TestNetmapForResponse(t *testing.T) {
|
||||
|
||||
@@ -67,7 +67,7 @@ type Status struct {
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message // nonempty when login finishes
|
||||
LogoutFinished *empty.Message // nonempty when logout finishes
|
||||
Err string
|
||||
Err error
|
||||
URL string // interactive URL to visit to finish logging in
|
||||
NetMap *netmap.NetworkMap // server-pushed configuration
|
||||
|
||||
|
||||
359
control/noise/conn.go
Normal file
359
control/noise/conn.go
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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 noise implements the base transport of the Tailscale 2021
|
||||
// control protocol.
|
||||
//
|
||||
// The base transport implements Noise IK, instantiated with
|
||||
// Curve25519, ChaCha20Poly1305 and BLAKE2s.
|
||||
package noise
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/blake2s"
|
||||
chp "golang.org/x/crypto/chacha20poly1305"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxMessageSize is the maximum size of a protocol frame on the
|
||||
// wire, including header and payload.
|
||||
maxMessageSize = 4096
|
||||
// maxCiphertextSize is the maximum amount of ciphertext bytes
|
||||
// that one protocol frame can carry, after framing.
|
||||
maxCiphertextSize = maxMessageSize - 3
|
||||
// maxPlaintextSize is the maximum amount of plaintext bytes that
|
||||
// one protocol frame can carry, after encryption and framing.
|
||||
maxPlaintextSize = maxCiphertextSize - chp.Overhead
|
||||
)
|
||||
|
||||
// A Conn is a secured Noise connection. It implements the net.Conn
|
||||
// interface, with the unusual trait that any write error (including a
|
||||
// SetWriteDeadline induced i/o timeout) causes all future writes to
|
||||
// fail.
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
version uint16
|
||||
peer key.MachinePublic
|
||||
handshakeHash [blake2s.Size]byte
|
||||
rx rxState
|
||||
tx txState
|
||||
}
|
||||
|
||||
// rxState is all the Conn state that Read uses.
|
||||
type rxState struct {
|
||||
sync.Mutex
|
||||
cipher cipher.AEAD
|
||||
nonce nonce
|
||||
buf [maxMessageSize]byte
|
||||
n int // number of valid bytes in buf
|
||||
next int // offset of next undecrypted packet
|
||||
plaintext []byte // slice into buf of decrypted bytes
|
||||
}
|
||||
|
||||
// txState is all the Conn state that Write uses.
|
||||
type txState struct {
|
||||
sync.Mutex
|
||||
cipher cipher.AEAD
|
||||
nonce nonce
|
||||
buf [maxMessageSize]byte
|
||||
err error // records the first partial write error for all future calls
|
||||
}
|
||||
|
||||
// ProtocolVersion returns the protocol version that was used to
|
||||
// establish this Conn.
|
||||
func (c *Conn) ProtocolVersion() int {
|
||||
return int(c.version)
|
||||
}
|
||||
|
||||
// HandshakeHash returns the Noise handshake hash for the connection,
|
||||
// which can be used to bind other messages to this connection
|
||||
// (i.e. to ensure that the message wasn't replayed from a different
|
||||
// connection).
|
||||
func (c *Conn) HandshakeHash() [blake2s.Size]byte {
|
||||
return c.handshakeHash
|
||||
}
|
||||
|
||||
// Peer returns the peer's long-term public key.
|
||||
func (c *Conn) Peer() key.MachinePublic {
|
||||
return c.peer
|
||||
}
|
||||
|
||||
// readNLocked reads into c.rx.buf until buf contains at least total
|
||||
// bytes. Returns a slice of the total bytes in rxBuf, or an
|
||||
// error if fewer than total bytes are available.
|
||||
func (c *Conn) readNLocked(total int) ([]byte, error) {
|
||||
if total > maxMessageSize {
|
||||
return nil, errReadTooBig{total}
|
||||
}
|
||||
for {
|
||||
if total <= c.rx.n {
|
||||
return c.rx.buf[:total], nil
|
||||
}
|
||||
|
||||
n, err := c.conn.Read(c.rx.buf[c.rx.n:])
|
||||
c.rx.n += n
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decryptLocked decrypts msg (which is header+ciphertext) in-place
|
||||
// and sets c.rx.plaintext to the decrypted bytes.
|
||||
func (c *Conn) decryptLocked(msg []byte) (err error) {
|
||||
if msgType := msg[0]; msgType != msgTypeRecord {
|
||||
return fmt.Errorf("received message with unexpected type %d, want %d", msgType, msgTypeRecord)
|
||||
}
|
||||
// We don't check the length field here, because the caller
|
||||
// already did in order to figure out how big the msg slice should
|
||||
// be.
|
||||
ciphertext := msg[headerLen:]
|
||||
|
||||
if !c.rx.nonce.Valid() {
|
||||
return errCipherExhausted{}
|
||||
}
|
||||
|
||||
c.rx.plaintext, err = c.rx.cipher.Open(ciphertext[:0], c.rx.nonce[:], ciphertext, nil)
|
||||
c.rx.nonce.Increment()
|
||||
|
||||
if err != nil {
|
||||
// Once a decryption has failed, our Conn is no longer
|
||||
// synchronized with our peer. Nuke the cipher state to be
|
||||
// safe, so that no further decryptions are attempted. Future
|
||||
// read attempts will return net.ErrClosed.
|
||||
c.rx.cipher = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// encryptLocked encrypts plaintext into c.tx.buf (including the
|
||||
// packet header) and returns a slice of the ciphertext, or an error
|
||||
// if the cipher is exhausted (i.e. can no longer be used safely).
|
||||
func (c *Conn) encryptLocked(plaintext []byte) ([]byte, error) {
|
||||
if !c.tx.nonce.Valid() {
|
||||
// Received 2^64-1 messages on this cipher state. Connection
|
||||
// is no longer usable.
|
||||
return nil, errCipherExhausted{}
|
||||
}
|
||||
|
||||
c.tx.buf[0] = msgTypeRecord
|
||||
binary.BigEndian.PutUint16(c.tx.buf[1:headerLen], uint16(len(plaintext)+chp.Overhead))
|
||||
ret := c.tx.cipher.Seal(c.tx.buf[:headerLen], c.tx.nonce[:], plaintext, nil)
|
||||
c.tx.nonce.Increment()
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// wholeMessageLocked returns a slice of one whole Noise transport
|
||||
// message from c.rx.buf, if one whole message is available, and
|
||||
// advances the read state to the next Noise message in the
|
||||
// buffer. Returns nil without advancing read state if there isn't one
|
||||
// whole message in c.rx.buf.
|
||||
func (c *Conn) wholeMessageLocked() []byte {
|
||||
available := c.rx.n - c.rx.next
|
||||
if available < headerLen {
|
||||
return nil
|
||||
}
|
||||
bs := c.rx.buf[c.rx.next:c.rx.n]
|
||||
totalSize := headerLen + int(binary.BigEndian.Uint16(bs[1:3]))
|
||||
if len(bs) < totalSize {
|
||||
return nil
|
||||
}
|
||||
c.rx.next += totalSize
|
||||
return bs[:totalSize]
|
||||
}
|
||||
|
||||
// decryptOneLocked decrypts one Noise transport message, reading from
|
||||
// c.conn as needed, and sets c.rx.plaintext to point to the decrypted
|
||||
// bytes. c.rx.plaintext is only valid if err == nil.
|
||||
func (c *Conn) decryptOneLocked() error {
|
||||
c.rx.plaintext = nil
|
||||
|
||||
// Fast path: do we have one whole ciphertext frame buffered
|
||||
// already?
|
||||
if bs := c.wholeMessageLocked(); bs != nil {
|
||||
return c.decryptLocked(bs)
|
||||
}
|
||||
|
||||
if c.rx.next != 0 {
|
||||
// To simplify the read logic, move the remainder of the
|
||||
// buffered bytes back to the head of the buffer, so we can
|
||||
// grow it without worrying about wraparound.
|
||||
c.rx.n = copy(c.rx.buf[:], c.rx.buf[c.rx.next:c.rx.n])
|
||||
c.rx.next = 0
|
||||
}
|
||||
|
||||
bs, err := c.readNLocked(headerLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The rest of the header (besides the length field) gets verified
|
||||
// in decryptLocked, not here.
|
||||
messageLen := headerLen + int(binary.BigEndian.Uint16(bs[1:3]))
|
||||
bs, err = c.readNLocked(messageLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.rx.next = len(bs)
|
||||
|
||||
return c.decryptLocked(bs)
|
||||
}
|
||||
|
||||
// Read implements io.Reader.
|
||||
func (c *Conn) Read(bs []byte) (int, error) {
|
||||
c.rx.Lock()
|
||||
defer c.rx.Unlock()
|
||||
|
||||
if c.rx.cipher == nil {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
// If no plaintext is buffered, decrypt incoming frames until we
|
||||
// have some plaintext. Zero-byte Noise frames are allowed in this
|
||||
// protocol, which is why we have to loop here rather than decrypt
|
||||
// a single additional frame.
|
||||
for len(c.rx.plaintext) == 0 {
|
||||
if err := c.decryptOneLocked(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
n := copy(bs, c.rx.plaintext)
|
||||
c.rx.plaintext = c.rx.plaintext[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (c *Conn) Write(bs []byte) (n int, err error) {
|
||||
c.tx.Lock()
|
||||
defer c.tx.Unlock()
|
||||
|
||||
if c.tx.err != nil {
|
||||
return 0, c.tx.err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// All write errors are fatal for this conn, so clear the
|
||||
// cipher state whenever an error happens.
|
||||
c.tx.cipher = nil
|
||||
}
|
||||
if c.tx.err == nil {
|
||||
// Only set c.tx.err if not nil so that we can return one
|
||||
// error on the first failure, and a different one for
|
||||
// subsequent calls. See the error handling around Write
|
||||
// below for why.
|
||||
c.tx.err = err
|
||||
}
|
||||
}()
|
||||
|
||||
if c.tx.cipher == nil {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
|
||||
var sent int
|
||||
for len(bs) > 0 {
|
||||
toSend := bs
|
||||
if len(toSend) > maxPlaintextSize {
|
||||
toSend = bs[:maxPlaintextSize]
|
||||
}
|
||||
bs = bs[len(toSend):]
|
||||
|
||||
ciphertext, err := c.encryptLocked(toSend)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := c.conn.Write(ciphertext)
|
||||
sent += n
|
||||
if err != nil {
|
||||
// Return the raw error on the Write that actually
|
||||
// failed. For future writes, return that error wrapped in
|
||||
// a desync error.
|
||||
c.tx.err = errPartialWrite{err}
|
||||
return sent, err
|
||||
}
|
||||
}
|
||||
return sent, nil
|
||||
}
|
||||
|
||||
// Close implements io.Closer.
|
||||
func (c *Conn) Close() error {
|
||||
closeErr := c.conn.Close() // unblocks any waiting reads or writes
|
||||
|
||||
// Remove references to live cipher state. Strictly speaking this
|
||||
// is unnecessary, but we want to try and hand the active cipher
|
||||
// state to the garbage collector promptly, to preserve perfect
|
||||
// forward secrecy as much as we can.
|
||||
c.rx.Lock()
|
||||
c.rx.cipher = nil
|
||||
c.rx.Unlock()
|
||||
c.tx.Lock()
|
||||
c.tx.cipher = nil
|
||||
c.tx.Unlock()
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() net.Addr { return c.conn.LocalAddr() }
|
||||
func (c *Conn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() }
|
||||
func (c *Conn) SetDeadline(t time.Time) error { return c.conn.SetDeadline(t) }
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) }
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) }
|
||||
|
||||
// errCipherExhausted is the error returned when we run out of nonces
|
||||
// on a cipher.
|
||||
type errCipherExhausted struct{}
|
||||
|
||||
func (errCipherExhausted) Error() string {
|
||||
return "cipher exhausted, no more nonces available for current key"
|
||||
}
|
||||
func (errCipherExhausted) Timeout() bool { return false }
|
||||
func (errCipherExhausted) Temporary() bool { return false }
|
||||
|
||||
// errPartialWrite is the error returned when the cipher state has
|
||||
// become unusable due to a past partial write.
|
||||
type errPartialWrite struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e errPartialWrite) Error() string {
|
||||
return fmt.Sprintf("cipher state desynchronized due to partial write (%v)", e.err)
|
||||
}
|
||||
func (e errPartialWrite) Unwrap() error { return e.err }
|
||||
func (e errPartialWrite) Temporary() bool { return false }
|
||||
func (e errPartialWrite) Timeout() bool { return false }
|
||||
|
||||
// errReadTooBig is the error returned when the peer sent an
|
||||
// unacceptably large Noise frame.
|
||||
type errReadTooBig struct {
|
||||
requested int
|
||||
}
|
||||
|
||||
func (e errReadTooBig) Error() string {
|
||||
return fmt.Sprintf("requested read of %d bytes exceeds max allowed Noise frame size", e.requested)
|
||||
}
|
||||
func (e errReadTooBig) Temporary() bool {
|
||||
// permanent error because this error only occurs when our peer
|
||||
// sends us a frame so large we're unwilling to ever decode it.
|
||||
return false
|
||||
}
|
||||
func (e errReadTooBig) Timeout() bool { return false }
|
||||
|
||||
type nonce [chp.NonceSize]byte
|
||||
|
||||
func (n *nonce) Valid() bool {
|
||||
return binary.BigEndian.Uint32(n[:4]) == 0 && binary.BigEndian.Uint64(n[4:]) != invalidNonce
|
||||
}
|
||||
|
||||
func (n *nonce) Increment() {
|
||||
if !n.Valid() {
|
||||
panic("increment of invalid nonce")
|
||||
}
|
||||
binary.BigEndian.PutUint64(n[4:], 1+binary.BigEndian.Uint64(n[4:]))
|
||||
}
|
||||
339
control/noise/conn_test.go
Normal file
339
control/noise/conn_test.go
Normal file
@@ -0,0 +1,339 @@
|
||||
// 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 noise
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
|
||||
chp "golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/net/nettest"
|
||||
tsnettest "tailscale.com/net/nettest"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestMessageSize(t *testing.T) {
|
||||
// This test is a regression guard against someone looking at
|
||||
// maxCiphertextSize, going "huh, we could be more efficient if it
|
||||
// were larger, and accidentally violating the Noise spec. Do not
|
||||
// change this max value, it's a deliberate limitation of the
|
||||
// cryptographic protocol we use (see Section 3 "Message Format"
|
||||
// of the Noise spec).
|
||||
const max = 65535
|
||||
if maxCiphertextSize > max {
|
||||
t.Fatalf("max ciphertext size is %d, which is larger than the maximum noise message size %d", maxCiphertextSize, max)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnBasic(t *testing.T) {
|
||||
client, server := pair(t)
|
||||
|
||||
sb := sinkReads(server)
|
||||
|
||||
want := "test"
|
||||
if _, err := io.WriteString(client, want); err != nil {
|
||||
t.Fatalf("client write failed: %v", err)
|
||||
}
|
||||
client.Close()
|
||||
|
||||
if got := sb.String(4); got != want {
|
||||
t.Fatalf("wrong content received: got %q, want %q", got, want)
|
||||
}
|
||||
if err := sb.Error(); err != io.EOF {
|
||||
t.Fatal("client close wasn't seen by server")
|
||||
}
|
||||
if sb.Total() != 4 {
|
||||
t.Fatalf("wrong amount of bytes received: got %d, want 4", sb.Total())
|
||||
}
|
||||
}
|
||||
|
||||
// bufferedWriteConn wraps a net.Conn and gives control over how
|
||||
// Writes get batched out.
|
||||
type bufferedWriteConn struct {
|
||||
net.Conn
|
||||
w *bufio.Writer
|
||||
manualFlush bool
|
||||
}
|
||||
|
||||
func (c *bufferedWriteConn) Write(bs []byte) (int, error) {
|
||||
n, err := c.w.Write(bs)
|
||||
if err == nil && !c.manualFlush {
|
||||
err = c.w.Flush()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// TestFastPath exercises the Read codepath that can receive multiple
|
||||
// Noise frames at once and decode each in turn without making another
|
||||
// syscall.
|
||||
func TestFastPath(t *testing.T) {
|
||||
s1, s2 := tsnettest.NewConn("noise", 128000)
|
||||
b := &bufferedWriteConn{s1, bufio.NewWriterSize(s1, 10000), false}
|
||||
client, server := pairWithConns(t, b, s2)
|
||||
|
||||
b.manualFlush = true
|
||||
|
||||
sb := sinkReads(server)
|
||||
|
||||
const packets = 10
|
||||
s := "test"
|
||||
for i := 0; i < packets; i++ {
|
||||
// Many separate writes, to force separate Noise frames that
|
||||
// all get buffered up and then all sent as a single slice to
|
||||
// the server.
|
||||
if _, err := io.WriteString(client, s); err != nil {
|
||||
t.Fatalf("client write1 failed: %v", err)
|
||||
}
|
||||
}
|
||||
if err := b.w.Flush(); err != nil {
|
||||
t.Fatalf("client flush failed: %v", err)
|
||||
}
|
||||
client.Close()
|
||||
|
||||
want := strings.Repeat(s, packets)
|
||||
if got := sb.String(len(want)); got != want {
|
||||
t.Fatalf("wrong content received: got %q, want %q", got, want)
|
||||
}
|
||||
if err := sb.Error(); err != io.EOF {
|
||||
t.Fatalf("client close wasn't seen by server")
|
||||
}
|
||||
}
|
||||
|
||||
// Writes things larger than a single Noise frame, to check the
|
||||
// chunking on the encoder and decoder.
|
||||
func TestBigData(t *testing.T) {
|
||||
client, server := pair(t)
|
||||
|
||||
serverReads := sinkReads(server)
|
||||
clientReads := sinkReads(client)
|
||||
|
||||
const sz = 15 * 1024 // 15KiB
|
||||
clientStr := strings.Repeat("abcde", sz/5)
|
||||
serverStr := strings.Repeat("fghij", sz/5*2)
|
||||
|
||||
if _, err := io.WriteString(client, clientStr); err != nil {
|
||||
t.Fatalf("writing client>server: %v", err)
|
||||
}
|
||||
if _, err := io.WriteString(server, serverStr); err != nil {
|
||||
t.Fatalf("writing server>client: %v", err)
|
||||
}
|
||||
|
||||
if serverGot := serverReads.String(sz); serverGot != clientStr {
|
||||
t.Error("server didn't receive what client sent")
|
||||
}
|
||||
if clientGot := clientReads.String(2 * sz); clientGot != serverStr {
|
||||
t.Error("client didn't receive what server sent")
|
||||
}
|
||||
|
||||
getNonce := func(n [chp.NonceSize]byte) uint64 {
|
||||
if binary.BigEndian.Uint32(n[:4]) != 0 {
|
||||
panic("unexpected nonce")
|
||||
}
|
||||
return binary.BigEndian.Uint64(n[4:])
|
||||
}
|
||||
|
||||
// Reach into the Conns and verify the cipher nonces advanced as
|
||||
// expected.
|
||||
if getNonce(client.tx.nonce) != getNonce(server.rx.nonce) {
|
||||
t.Error("desynchronized client tx nonce")
|
||||
}
|
||||
if getNonce(server.tx.nonce) != getNonce(client.rx.nonce) {
|
||||
t.Error("desynchronized server tx nonce")
|
||||
}
|
||||
if n := getNonce(client.tx.nonce); n != 4 {
|
||||
t.Errorf("wrong client tx nonce, got %d want 4", n)
|
||||
}
|
||||
if n := getNonce(server.tx.nonce); n != 8 {
|
||||
t.Errorf("wrong client tx nonce, got %d want 8", n)
|
||||
}
|
||||
}
|
||||
|
||||
// readerConn wraps a net.Conn and routes its Reads through a separate
|
||||
// io.Reader.
|
||||
type readerConn struct {
|
||||
net.Conn
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (c readerConn) Read(bs []byte) (int, error) { return c.r.Read(bs) }
|
||||
|
||||
// Check that the receiver can handle not being able to read an entire
|
||||
// frame in a single syscall.
|
||||
func TestDataTrickle(t *testing.T) {
|
||||
s1, s2 := tsnettest.NewConn("noise", 128000)
|
||||
client, server := pairWithConns(t, s1, readerConn{s2, iotest.OneByteReader(s2)})
|
||||
serverReads := sinkReads(server)
|
||||
|
||||
const sz = 10000
|
||||
clientStr := strings.Repeat("abcde", sz/5)
|
||||
if _, err := io.WriteString(client, clientStr); err != nil {
|
||||
t.Fatalf("writing client>server: %v", err)
|
||||
}
|
||||
|
||||
serverGot := serverReads.String(sz)
|
||||
if serverGot != clientStr {
|
||||
t.Error("server didn't receive what client sent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnStd(t *testing.T) {
|
||||
// You can run this test manually, and noise.Conn should pass all
|
||||
// of them except for TestConn/PastTimeout,
|
||||
// TestConn/FutureTimeout, TestConn/ConcurrentMethods, because
|
||||
// those tests assume that write errors are recoverable, and
|
||||
// they're not on our Conn due to cipher security.
|
||||
t.Skip("not all tests can pass on this Conn, see https://github.com/golang/go/issues/46977")
|
||||
nettest.TestConn(t, func() (c1 net.Conn, c2 net.Conn, stop func(), err error) {
|
||||
s1, s2 := tsnettest.NewConn("noise", 4096)
|
||||
controlKey := key.NewMachine()
|
||||
machineKey := key.NewMachine()
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
var err error
|
||||
c2, err = Server(context.Background(), s2, controlKey)
|
||||
serverErr <- err
|
||||
}()
|
||||
c1, err = Client(context.Background(), s1, machineKey, controlKey.Public())
|
||||
if err != nil {
|
||||
s1.Close()
|
||||
s2.Close()
|
||||
return nil, nil, nil, fmt.Errorf("connecting client: %w", err)
|
||||
}
|
||||
if err := <-serverErr; err != nil {
|
||||
c1.Close()
|
||||
s1.Close()
|
||||
s2.Close()
|
||||
return nil, nil, nil, fmt.Errorf("connecting server: %w", err)
|
||||
}
|
||||
return c1, c2, func() {
|
||||
c1.Close()
|
||||
c2.Close()
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// mkConns creates synthetic Noise Conns wrapping the given net.Conns.
|
||||
// This function is for testing just the Conn transport logic without
|
||||
// having to muck about with Noise handshakes.
|
||||
func mkConns(s1, s2 net.Conn) (*Conn, *Conn) {
|
||||
var k1, k2 [chp.KeySize]byte
|
||||
if _, err := rand.Read(k1[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := rand.Read(k2[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ret1 := &Conn{
|
||||
conn: s1,
|
||||
tx: txState{cipher: newCHP(k1)},
|
||||
rx: rxState{cipher: newCHP(k2)},
|
||||
}
|
||||
ret2 := &Conn{
|
||||
conn: s2,
|
||||
tx: txState{cipher: newCHP(k2)},
|
||||
rx: rxState{cipher: newCHP(k1)},
|
||||
}
|
||||
|
||||
return ret1, ret2
|
||||
}
|
||||
|
||||
type readSink struct {
|
||||
r io.Reader
|
||||
|
||||
cond *sync.Cond
|
||||
sync.Mutex
|
||||
bs bytes.Buffer
|
||||
err error
|
||||
}
|
||||
|
||||
func sinkReads(r io.Reader) *readSink {
|
||||
ret := &readSink{
|
||||
r: r,
|
||||
}
|
||||
ret.cond = sync.NewCond(&ret.Mutex)
|
||||
go func() {
|
||||
var buf [4096]byte
|
||||
for {
|
||||
n, err := r.Read(buf[:])
|
||||
ret.Lock()
|
||||
ret.bs.Write(buf[:n])
|
||||
if err != nil {
|
||||
ret.err = err
|
||||
}
|
||||
ret.cond.Broadcast()
|
||||
ret.Unlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *readSink) String(total int) string {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
for s.bs.Len() < total && s.err == nil {
|
||||
s.cond.Wait()
|
||||
}
|
||||
if s.err != nil {
|
||||
total = s.bs.Len()
|
||||
}
|
||||
return string(s.bs.Bytes()[:total])
|
||||
}
|
||||
|
||||
func (s *readSink) Error() error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
for s.err == nil {
|
||||
s.cond.Wait()
|
||||
}
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *readSink) Total() int {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.bs.Len()
|
||||
}
|
||||
|
||||
func pairWithConns(t *testing.T, clientConn, serverConn net.Conn) (*Conn, *Conn) {
|
||||
var (
|
||||
controlKey = key.NewMachine()
|
||||
machineKey = key.NewMachine()
|
||||
server *Conn
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
var err error
|
||||
server, err = Server(context.Background(), serverConn, controlKey)
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
client, err := Client(context.Background(), clientConn, machineKey, controlKey.Public())
|
||||
if err != nil {
|
||||
t.Fatalf("client connection failed: %v", err)
|
||||
}
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatalf("server connection failed: %v", err)
|
||||
}
|
||||
return client, server
|
||||
}
|
||||
|
||||
func pair(t *testing.T) (*Conn, *Conn) {
|
||||
s1, s2 := tsnettest.NewConn("noise", 128000)
|
||||
return pairWithConns(t, s1, s2)
|
||||
}
|
||||
443
control/noise/handshake.go
Normal file
443
control/noise/handshake.go
Normal file
@@ -0,0 +1,443 @@
|
||||
// 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 noise
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
chp "golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
const (
|
||||
// protocolName is the name of the specific instantiation of Noise
|
||||
// that the control protocol uses. This string's value is fixed by
|
||||
// the Noise spec, and shouldn't be changed unless we're updating
|
||||
// the control protocol to use a different Noise instance.
|
||||
protocolName = "Noise_IK_25519_ChaChaPoly_BLAKE2s"
|
||||
// protocolVersion is the version of the control protocol that
|
||||
// Client will use when initiating a handshake.
|
||||
protocolVersion uint16 = 1
|
||||
// protocolVersionPrefix is the name portion of the protocol
|
||||
// name+version string that gets mixed into the handshake as a
|
||||
// prologue.
|
||||
//
|
||||
// This mixing verifies that both clients agree that they're
|
||||
// executing the control protocol at a specific version that
|
||||
// matches the advertised version in the cleartext packet header.
|
||||
protocolVersionPrefix = "Tailscale Control Protocol v"
|
||||
invalidNonce = ^uint64(0)
|
||||
)
|
||||
|
||||
func protocolVersionPrologue(version uint16) []byte {
|
||||
ret := make([]byte, 0, len(protocolVersionPrefix)+5) // 5 bytes is enough to encode all possible version numbers.
|
||||
ret = append(ret, protocolVersionPrefix...)
|
||||
return strconv.AppendUint(ret, uint64(version), 10)
|
||||
}
|
||||
|
||||
// Client initiates a control client handshake, returning the resulting
|
||||
// control connection.
|
||||
//
|
||||
// The context deadline, if any, covers the entire handshaking
|
||||
// process. Any preexisting Conn deadline is removed.
|
||||
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return nil, fmt.Errorf("setting conn deadline: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
conn.SetDeadline(time.Time{})
|
||||
}()
|
||||
}
|
||||
|
||||
var s symmetricState
|
||||
s.Initialize()
|
||||
|
||||
// prologue
|
||||
s.MixHash(protocolVersionPrologue(protocolVersion))
|
||||
|
||||
// <- s
|
||||
// ...
|
||||
s.MixHash(controlKey.UntypedBytes())
|
||||
|
||||
// -> e, es, s, ss
|
||||
init := mkInitiationMessage()
|
||||
machineEphemeral := key.NewMachine()
|
||||
machineEphemeralPub := machineEphemeral.Public()
|
||||
copy(init.EphemeralPub(), machineEphemeralPub.UntypedBytes())
|
||||
s.MixHash(machineEphemeralPub.UntypedBytes())
|
||||
cipher, err := s.MixDH(machineEphemeral, controlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing es: %w", err)
|
||||
}
|
||||
machineKeyPub := machineKey.Public()
|
||||
s.EncryptAndHash(cipher, init.MachinePub(), machineKeyPub.UntypedBytes())
|
||||
cipher, err = s.MixDH(machineKey, controlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing ss: %w", err)
|
||||
}
|
||||
s.EncryptAndHash(cipher, init.Tag(), nil) // empty message payload
|
||||
|
||||
if _, err := conn.Write(init[:]); err != nil {
|
||||
return nil, fmt.Errorf("writing initiation: %w", err)
|
||||
}
|
||||
|
||||
// Read in the payload and look for errors/protocol violations from the server.
|
||||
var resp responseMessage
|
||||
if _, err := io.ReadFull(conn, resp.Header()); err != nil {
|
||||
return nil, fmt.Errorf("reading response header: %w", err)
|
||||
}
|
||||
if resp.Type() != msgTypeResponse {
|
||||
if resp.Type() != msgTypeError {
|
||||
return nil, fmt.Errorf("unexpected response message type %d", resp.Type())
|
||||
}
|
||||
msg := make([]byte, resp.Length())
|
||||
if _, err := io.ReadFull(conn, msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("server error: %q", msg)
|
||||
}
|
||||
if resp.Length() != len(resp.Payload()) {
|
||||
return nil, fmt.Errorf("wrong length %d received for handshake response", resp.Length())
|
||||
}
|
||||
if _, err := io.ReadFull(conn, resp.Payload()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// <- e, ee, se
|
||||
controlEphemeralPub := key.MachinePublicFromRaw32(mem.B(resp.EphemeralPub()))
|
||||
s.MixHash(controlEphemeralPub.UntypedBytes())
|
||||
if _, err = s.MixDH(machineEphemeral, controlEphemeralPub); err != nil {
|
||||
return nil, fmt.Errorf("computing ee: %w", err)
|
||||
}
|
||||
cipher, err = s.MixDH(machineKey, controlEphemeralPub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing se: %w", err)
|
||||
}
|
||||
if err := s.DecryptAndHash(cipher, nil, resp.Tag()); err != nil {
|
||||
return nil, fmt.Errorf("decrypting payload: %w", err)
|
||||
}
|
||||
|
||||
c1, c2, err := s.Split()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finalizing handshake: %w", err)
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
version: protocolVersion,
|
||||
peer: controlKey,
|
||||
handshakeHash: s.h,
|
||||
tx: txState{
|
||||
cipher: c1,
|
||||
},
|
||||
rx: rxState{
|
||||
cipher: c2,
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Server initiates a control server handshake, returning the resulting
|
||||
// control connection.
|
||||
//
|
||||
// The context deadline, if any, covers the entire handshaking
|
||||
// process.
|
||||
func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate) (*Conn, error) {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return nil, fmt.Errorf("setting conn deadline: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
conn.SetDeadline(time.Time{})
|
||||
}()
|
||||
}
|
||||
|
||||
// Deliberately does not support formatting, so that we don't echo
|
||||
// attacker-controlled input back to them.
|
||||
sendErr := func(msg string) error {
|
||||
if len(msg) >= 1<<16 {
|
||||
msg = msg[:1<<16]
|
||||
}
|
||||
var hdr [headerLen]byte
|
||||
hdr[0] = msgTypeError
|
||||
binary.BigEndian.PutUint16(hdr[1:3], uint16(len(msg)))
|
||||
if _, err := conn.Write(hdr[:]); err != nil {
|
||||
return fmt.Errorf("sending %q error to client: %w", msg, err)
|
||||
}
|
||||
if _, err := io.WriteString(conn, msg); err != nil {
|
||||
return fmt.Errorf("sending %q error to client: %w", msg, err)
|
||||
}
|
||||
return fmt.Errorf("refused client handshake: %q", msg)
|
||||
}
|
||||
|
||||
var s symmetricState
|
||||
s.Initialize()
|
||||
|
||||
var init initiationMessage
|
||||
if _, err := io.ReadFull(conn, init.Header()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if init.Version() != protocolVersion {
|
||||
return nil, sendErr("unsupported protocol version")
|
||||
}
|
||||
if init.Type() != msgTypeInitiation {
|
||||
return nil, sendErr("unexpected handshake message type")
|
||||
}
|
||||
if init.Length() != len(init.Payload()) {
|
||||
return nil, sendErr("wrong handshake initiation length")
|
||||
}
|
||||
if _, err := io.ReadFull(conn, init.Payload()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// prologue. Can only do this once we at least think the client is
|
||||
// handshaking using a supported version.
|
||||
s.MixHash(protocolVersionPrologue(protocolVersion))
|
||||
|
||||
// <- s
|
||||
// ...
|
||||
controlKeyPub := controlKey.Public()
|
||||
s.MixHash(controlKeyPub.UntypedBytes())
|
||||
|
||||
// -> e, es, s, ss
|
||||
machineEphemeralPub := key.MachinePublicFromRaw32(mem.B(init.EphemeralPub()))
|
||||
s.MixHash(machineEphemeralPub.UntypedBytes())
|
||||
cipher, err := s.MixDH(controlKey, machineEphemeralPub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing es: %w", err)
|
||||
}
|
||||
var machineKeyBytes [32]byte
|
||||
if err := s.DecryptAndHash(cipher, machineKeyBytes[:], init.MachinePub()); err != nil {
|
||||
return nil, fmt.Errorf("decrypting machine key: %w", err)
|
||||
}
|
||||
machineKey := key.MachinePublicFromRaw32(mem.B(machineKeyBytes[:]))
|
||||
cipher, err = s.MixDH(controlKey, machineKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing ss: %w", err)
|
||||
}
|
||||
if err := s.DecryptAndHash(cipher, nil, init.Tag()); err != nil {
|
||||
return nil, fmt.Errorf("decrypting initiation tag: %w", err)
|
||||
}
|
||||
|
||||
// <- e, ee, se
|
||||
resp := mkResponseMessage()
|
||||
controlEphemeral := key.NewMachine()
|
||||
controlEphemeralPub := controlEphemeral.Public()
|
||||
copy(resp.EphemeralPub(), controlEphemeralPub.UntypedBytes())
|
||||
s.MixHash(controlEphemeralPub.UntypedBytes())
|
||||
if _, err := s.MixDH(controlEphemeral, machineEphemeralPub); err != nil {
|
||||
return nil, fmt.Errorf("computing ee: %w", err)
|
||||
}
|
||||
cipher, err = s.MixDH(controlEphemeral, machineKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing se: %w", err)
|
||||
}
|
||||
s.EncryptAndHash(cipher, resp.Tag(), nil) // empty message payload
|
||||
|
||||
c1, c2, err := s.Split()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finalizing handshake: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(resp[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
version: protocolVersion,
|
||||
peer: machineKey,
|
||||
handshakeHash: s.h,
|
||||
tx: txState{
|
||||
cipher: c2,
|
||||
},
|
||||
rx: rxState{
|
||||
cipher: c1,
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// symmetricState contains the state of an in-flight handshake.
|
||||
type symmetricState struct {
|
||||
finished bool
|
||||
|
||||
h [blake2s.Size]byte // hash of currently-processed handshake state
|
||||
ck [blake2s.Size]byte // chaining key used to construct session keys at the end of the handshake
|
||||
}
|
||||
|
||||
func (s *symmetricState) checkFinished() {
|
||||
if s.finished {
|
||||
panic("attempted to use symmetricState after Split was called")
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize sets s to the initial handshake state, prior to
|
||||
// processing any handshake messages.
|
||||
func (s *symmetricState) Initialize() {
|
||||
s.checkFinished()
|
||||
s.h = blake2s.Sum256([]byte(protocolName))
|
||||
s.ck = s.h
|
||||
}
|
||||
|
||||
// MixHash updates s.h to be BLAKE2s(s.h || data), where || is
|
||||
// concatenation.
|
||||
func (s *symmetricState) MixHash(data []byte) {
|
||||
s.checkFinished()
|
||||
h := newBLAKE2s()
|
||||
h.Write(s.h[:])
|
||||
h.Write(data)
|
||||
h.Sum(s.h[:0])
|
||||
}
|
||||
|
||||
// MixDH updates s.ck with the result of X25519(priv, pub) and returns
|
||||
// a singleUseCHP that can be used to encrypt or decrypt handshake
|
||||
// data.
|
||||
//
|
||||
// MixDH corresponds to MixKey(X25519(...))) in the spec. Implementing
|
||||
// it as a single function allows for strongly-typed arguments that
|
||||
// reduce the risk of error in the caller (e.g. invoking X25519 with
|
||||
// two private keys, or two public keys), and thus producing the wrong
|
||||
// calculation.
|
||||
func (s *symmetricState) MixDH(priv key.MachinePrivate, pub key.MachinePublic) (*singleUseCHP, error) {
|
||||
s.checkFinished()
|
||||
keyData, err := curve25519.X25519(priv.UntypedBytes(), pub.UntypedBytes())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing X25519: %w", err)
|
||||
}
|
||||
|
||||
r := hkdf.New(newBLAKE2s, keyData, s.ck[:], nil)
|
||||
if _, err := io.ReadFull(r, s.ck[:]); err != nil {
|
||||
return nil, fmt.Errorf("extracting ck: %w", err)
|
||||
}
|
||||
var k [chp.KeySize]byte
|
||||
if _, err := io.ReadFull(r, k[:]); err != nil {
|
||||
return nil, fmt.Errorf("extracting k: %w", err)
|
||||
}
|
||||
return newSingleUseCHP(k), nil
|
||||
}
|
||||
|
||||
// EncryptAndHash encrypts plaintext into ciphertext (which must be
|
||||
// the correct size to hold the encrypted plaintext) using cipher,
|
||||
// mixes the ciphertext into s.h, and returns the ciphertext.
|
||||
func (s *symmetricState) EncryptAndHash(cipher *singleUseCHP, ciphertext, plaintext []byte) {
|
||||
s.checkFinished()
|
||||
if len(ciphertext) != len(plaintext)+chp.Overhead {
|
||||
panic("ciphertext is wrong size for given plaintext")
|
||||
}
|
||||
ret := cipher.Seal(ciphertext[:0], plaintext, s.h[:])
|
||||
s.MixHash(ret)
|
||||
}
|
||||
|
||||
// DecryptAndHash decrypts the given ciphertext into plaintext (which
|
||||
// must be the correct size to hold the decrypted ciphertext) using
|
||||
// cipher. If decryption is successful, it mixes the ciphertext into
|
||||
// s.h.
|
||||
func (s *symmetricState) DecryptAndHash(cipher *singleUseCHP, plaintext, ciphertext []byte) error {
|
||||
s.checkFinished()
|
||||
if len(ciphertext) != len(plaintext)+chp.Overhead {
|
||||
return errors.New("plaintext is wrong size for given ciphertext")
|
||||
}
|
||||
if _, err := cipher.Open(plaintext[:0], ciphertext, s.h[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
s.MixHash(ciphertext)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Split returns two ChaCha20Poly1305 ciphers with keys derived from
|
||||
// the current handshake state. Methods on s cannot be used again
|
||||
// after calling Split.
|
||||
func (s *symmetricState) Split() (c1, c2 cipher.AEAD, err error) {
|
||||
s.finished = true
|
||||
|
||||
var k1, k2 [chp.KeySize]byte
|
||||
r := hkdf.New(newBLAKE2s, nil, s.ck[:], nil)
|
||||
if _, err := io.ReadFull(r, k1[:]); err != nil {
|
||||
return nil, nil, fmt.Errorf("extracting k1: %w", err)
|
||||
}
|
||||
if _, err := io.ReadFull(r, k2[:]); err != nil {
|
||||
return nil, nil, fmt.Errorf("extracting k2: %w", err)
|
||||
}
|
||||
c1, err = chp.New(k1[:])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("constructing AEAD c1: %w", err)
|
||||
}
|
||||
c2, err = chp.New(k2[:])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("constructing AEAD c2: %w", err)
|
||||
}
|
||||
return c1, c2, nil
|
||||
}
|
||||
|
||||
// newBLAKE2s returns a hash.Hash implementing BLAKE2s, or panics on
|
||||
// error.
|
||||
func newBLAKE2s() hash.Hash {
|
||||
h, err := blake2s.New256(nil)
|
||||
if err != nil {
|
||||
// Should never happen, errors only happen when using BLAKE2s
|
||||
// in MAC mode with a key.
|
||||
panic(err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// newCHP returns a cipher.AEAD implementing ChaCha20Poly1305, or
|
||||
// panics on error.
|
||||
func newCHP(key [chp.KeySize]byte) cipher.AEAD {
|
||||
aead, err := chp.New(key[:])
|
||||
if err != nil {
|
||||
// Can only happen if we passed a key of the wrong length. The
|
||||
// function signature prevents that.
|
||||
panic(err)
|
||||
}
|
||||
return aead
|
||||
}
|
||||
|
||||
// singleUseCHP is an instance of ChaCha20Poly1305 that can be used
|
||||
// only once, either for encrypting or decrypting, but not both. The
|
||||
// chosen operation is always executed with an all-zeros
|
||||
// nonce. Subsequent calls to either Seal or Open panic.
|
||||
type singleUseCHP struct {
|
||||
c cipher.AEAD
|
||||
}
|
||||
|
||||
func newSingleUseCHP(key [chp.KeySize]byte) *singleUseCHP {
|
||||
return &singleUseCHP{newCHP(key)}
|
||||
}
|
||||
|
||||
func (c *singleUseCHP) Seal(dst, plaintext, additionalData []byte) []byte {
|
||||
if c.c == nil {
|
||||
panic("Attempted reuse of singleUseAEAD")
|
||||
}
|
||||
cipher := c.c
|
||||
c.c = nil
|
||||
var nonce [chp.NonceSize]byte
|
||||
return cipher.Seal(dst, nonce[:], plaintext, additionalData)
|
||||
}
|
||||
|
||||
func (c *singleUseCHP) Open(dst, ciphertext, additionalData []byte) ([]byte, error) {
|
||||
if c.c == nil {
|
||||
panic("Attempted reuse of singleUseAEAD")
|
||||
}
|
||||
cipher := c.c
|
||||
c.c = nil
|
||||
var nonce [chp.NonceSize]byte
|
||||
return cipher.Open(dst, nonce[:], ciphertext, additionalData)
|
||||
}
|
||||
296
control/noise/handshake_test.go
Normal file
296
control/noise/handshake_test.go
Normal file
@@ -0,0 +1,296 @@
|
||||
// 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 noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tsnettest "tailscale.com/net/nettest"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestHandshake(t *testing.T) {
|
||||
var (
|
||||
clientConn, serverConn = tsnettest.NewConn("noise", 128000)
|
||||
serverKey = key.NewMachine()
|
||||
clientKey = key.NewMachine()
|
||||
server *Conn
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
var err error
|
||||
server, err = Server(context.Background(), serverConn, serverKey)
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
|
||||
if err != nil {
|
||||
t.Fatalf("client connection failed: %v", err)
|
||||
}
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatalf("server connection failed: %v", err)
|
||||
}
|
||||
|
||||
if client.HandshakeHash() != server.HandshakeHash() {
|
||||
t.Fatal("client and server disagree on handshake hash")
|
||||
}
|
||||
|
||||
if client.ProtocolVersion() != int(protocolVersion) {
|
||||
t.Fatalf("client reporting wrong protocol version %d, want %d", client.ProtocolVersion(), protocolVersion)
|
||||
}
|
||||
if client.ProtocolVersion() != server.ProtocolVersion() {
|
||||
t.Fatalf("peers disagree on protocol version, client=%d server=%d", client.ProtocolVersion(), server.ProtocolVersion())
|
||||
}
|
||||
if client.Peer() != serverKey.Public() {
|
||||
t.Fatal("client peer key isn't serverKey")
|
||||
}
|
||||
if server.Peer() != clientKey.Public() {
|
||||
t.Fatal("client peer key isn't serverKey")
|
||||
}
|
||||
}
|
||||
|
||||
// Check that handshaking repeatedly with the same long-term keys
|
||||
// result in different handshake hashes and wire traffic.
|
||||
func TestNoReuse(t *testing.T) {
|
||||
var (
|
||||
hashes = map[[32]byte]bool{}
|
||||
clientHandshakes = map[[96]byte]bool{}
|
||||
serverHandshakes = map[[48]byte]bool{}
|
||||
packets = map[[32]byte]bool{}
|
||||
)
|
||||
for i := 0; i < 10; i++ {
|
||||
var (
|
||||
clientRaw, serverRaw = tsnettest.NewConn("noise", 128000)
|
||||
clientBuf, serverBuf bytes.Buffer
|
||||
clientConn = &readerConn{clientRaw, io.TeeReader(clientRaw, &clientBuf)}
|
||||
serverConn = &readerConn{serverRaw, io.TeeReader(serverRaw, &serverBuf)}
|
||||
serverKey = key.NewMachine()
|
||||
clientKey = key.NewMachine()
|
||||
server *Conn
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
var err error
|
||||
server, err = Server(context.Background(), serverConn, serverKey)
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
|
||||
if err != nil {
|
||||
t.Fatalf("client connection failed: %v", err)
|
||||
}
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatalf("server connection failed: %v", err)
|
||||
}
|
||||
|
||||
var clientHS [96]byte
|
||||
copy(clientHS[:], serverBuf.Bytes())
|
||||
if clientHandshakes[clientHS] {
|
||||
t.Fatal("client handshake seen twice")
|
||||
}
|
||||
clientHandshakes[clientHS] = true
|
||||
|
||||
var serverHS [48]byte
|
||||
copy(serverHS[:], clientBuf.Bytes())
|
||||
if serverHandshakes[serverHS] {
|
||||
t.Fatal("server handshake seen twice")
|
||||
}
|
||||
serverHandshakes[serverHS] = true
|
||||
|
||||
clientBuf.Reset()
|
||||
serverBuf.Reset()
|
||||
cb := sinkReads(client)
|
||||
sb := sinkReads(server)
|
||||
|
||||
if hashes[client.HandshakeHash()] {
|
||||
t.Fatalf("handshake hash %v seen twice", client.HandshakeHash())
|
||||
}
|
||||
hashes[client.HandshakeHash()] = true
|
||||
|
||||
// Sending 14 bytes turns into 32 bytes on the wire (+16 for
|
||||
// the chacha20poly1305 overhead, +2 length header)
|
||||
if _, err := io.WriteString(client, strings.Repeat("a", 14)); err != nil {
|
||||
t.Fatalf("client>server write failed: %v", err)
|
||||
}
|
||||
if _, err := io.WriteString(server, strings.Repeat("b", 14)); err != nil {
|
||||
t.Fatalf("server>client write failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait for the bytes to be read, so we know they've traveled end to end
|
||||
cb.String(14)
|
||||
sb.String(14)
|
||||
|
||||
var clientWire, serverWire [32]byte
|
||||
copy(clientWire[:], clientBuf.Bytes())
|
||||
copy(serverWire[:], serverBuf.Bytes())
|
||||
|
||||
if packets[clientWire] {
|
||||
t.Fatalf("client wire traffic seen twice")
|
||||
}
|
||||
packets[clientWire] = true
|
||||
if packets[serverWire] {
|
||||
t.Fatalf("server wire traffic seen twice")
|
||||
}
|
||||
packets[serverWire] = true
|
||||
}
|
||||
}
|
||||
|
||||
// tamperReader wraps a reader and mutates the Nth byte.
|
||||
type tamperReader struct {
|
||||
r io.Reader
|
||||
n int
|
||||
total int
|
||||
}
|
||||
|
||||
func (r *tamperReader) Read(bs []byte) (int, error) {
|
||||
n, err := r.r.Read(bs)
|
||||
if off := r.n - r.total; off >= 0 && off < n {
|
||||
bs[off] += 1
|
||||
}
|
||||
r.total += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func TestTampering(t *testing.T) {
|
||||
// Tamper with every byte of the client initiation message.
|
||||
for i := 0; i < 101; i++ {
|
||||
var (
|
||||
clientConn, serverRaw = tsnettest.NewConn("noise", 128000)
|
||||
serverConn = &readerConn{serverRaw, &tamperReader{serverRaw, i, 0}}
|
||||
serverKey = key.NewMachine()
|
||||
clientKey = key.NewMachine()
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
_, err := Server(context.Background(), serverConn, serverKey)
|
||||
// If the server failed, we have to close the Conn to
|
||||
// unblock the client.
|
||||
if err != nil {
|
||||
serverConn.Close()
|
||||
}
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
_, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
|
||||
if err == nil {
|
||||
t.Fatal("client connection succeeded despite tampering")
|
||||
}
|
||||
if err := <-serverErr; err == nil {
|
||||
t.Fatalf("server connection succeeded despite tampering")
|
||||
}
|
||||
}
|
||||
|
||||
// Tamper with every byte of the server response message.
|
||||
for i := 0; i < 51; i++ {
|
||||
var (
|
||||
clientRaw, serverConn = tsnettest.NewConn("noise", 128000)
|
||||
clientConn = &readerConn{clientRaw, &tamperReader{clientRaw, i, 0}}
|
||||
serverKey = key.NewMachine()
|
||||
clientKey = key.NewMachine()
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
_, err := Server(context.Background(), serverConn, serverKey)
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
_, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
|
||||
if err == nil {
|
||||
t.Fatal("client connection succeeded despite tampering")
|
||||
}
|
||||
// The server shouldn't fail, because the tampering took place
|
||||
// in its response.
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatalf("server connection failed despite no tampering: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tamper with every byte of the first server>client transport message.
|
||||
for i := 0; i < 30; i++ {
|
||||
var (
|
||||
clientRaw, serverConn = tsnettest.NewConn("noise", 128000)
|
||||
clientConn = &readerConn{clientRaw, &tamperReader{clientRaw, 51 + i, 0}}
|
||||
serverKey = key.NewMachine()
|
||||
clientKey = key.NewMachine()
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
server, err := Server(context.Background(), serverConn, serverKey)
|
||||
serverErr <- err
|
||||
_, err = io.WriteString(server, strings.Repeat("a", 14))
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
|
||||
if err != nil {
|
||||
t.Fatalf("client handshake failed: %v", err)
|
||||
}
|
||||
// The server shouldn't fail, because the tampering took place
|
||||
// in its response.
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatalf("server handshake failed: %v", err)
|
||||
}
|
||||
|
||||
// The client needs a timeout if the tampering is hitting the length header.
|
||||
if i == 1 || i == 2 {
|
||||
client.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
|
||||
}
|
||||
|
||||
var bs [100]byte
|
||||
n, err := client.Read(bs[:])
|
||||
if err == nil {
|
||||
t.Fatal("read succeeded despite tampering")
|
||||
}
|
||||
if n != 0 {
|
||||
t.Fatal("conn yielded some bytes despite tampering")
|
||||
}
|
||||
}
|
||||
|
||||
// Tamper with every byte of the first client>server transport message.
|
||||
for i := 0; i < 30; i++ {
|
||||
var (
|
||||
clientConn, serverRaw = tsnettest.NewConn("noise", 128000)
|
||||
serverConn = &readerConn{serverRaw, &tamperReader{serverRaw, 101 + i, 0}}
|
||||
serverKey = key.NewMachine()
|
||||
clientKey = key.NewMachine()
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
server, err := Server(context.Background(), serverConn, serverKey)
|
||||
serverErr <- err
|
||||
var bs [100]byte
|
||||
// The server needs a timeout if the tampering is hitting the length header.
|
||||
if i == 1 || i == 2 {
|
||||
server.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
|
||||
}
|
||||
n, err := server.Read(bs[:])
|
||||
if n != 0 {
|
||||
panic("server got bytes despite tampering")
|
||||
} else {
|
||||
serverErr <- err
|
||||
}
|
||||
}()
|
||||
|
||||
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
|
||||
if err != nil {
|
||||
t.Fatalf("client handshake failed: %v", err)
|
||||
}
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatalf("server handshake failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(client, strings.Repeat("a", 14)); err != nil {
|
||||
t.Fatalf("client>server write failed: %v", err)
|
||||
}
|
||||
if err := <-serverErr; err == nil {
|
||||
t.Fatal("server successfully received bytes despite tampering")
|
||||
}
|
||||
}
|
||||
}
|
||||
257
control/noise/interop_test.go
Normal file
257
control/noise/interop_test.go
Normal file
@@ -0,0 +1,257 @@
|
||||
// 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 noise
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
tsnettest "tailscale.com/net/nettest"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// Can a reference Noise IK client talk to our server?
|
||||
func TestInteropClient(t *testing.T) {
|
||||
var (
|
||||
s1, s2 = tsnettest.NewConn("noise", 128000)
|
||||
controlKey = key.NewMachine()
|
||||
machineKey = key.NewMachine()
|
||||
serverErr = make(chan error, 2)
|
||||
serverBytes = make(chan []byte, 1)
|
||||
c2s = "client>server"
|
||||
s2c = "server>client"
|
||||
)
|
||||
|
||||
go func() {
|
||||
server, err := Server(context.Background(), s2, controlKey)
|
||||
serverErr <- err
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var buf [1024]byte
|
||||
_, err = io.ReadFull(server, buf[:len(c2s)])
|
||||
serverBytes <- buf[:len(c2s)]
|
||||
if err != nil {
|
||||
serverErr <- err
|
||||
return
|
||||
}
|
||||
_, err = server.Write([]byte(s2c))
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
gotS2C, err := noiseExplorerClient(s1, controlKey.Public(), machineKey, []byte(c2s))
|
||||
if err != nil {
|
||||
t.Fatalf("failed client interop: %v", err)
|
||||
}
|
||||
if string(gotS2C) != s2c {
|
||||
t.Fatalf("server sent unexpected data %q, want %q", string(gotS2C), s2c)
|
||||
}
|
||||
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatalf("server handshake failed: %v", err)
|
||||
}
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatalf("server read/write failed: %v", err)
|
||||
}
|
||||
if got := string(<-serverBytes); got != c2s {
|
||||
t.Fatalf("server received %q, want %q", got, c2s)
|
||||
}
|
||||
}
|
||||
|
||||
// Can our client talk to a reference Noise IK server?
|
||||
func TestInteropServer(t *testing.T) {
|
||||
var (
|
||||
s1, s2 = tsnettest.NewConn("noise", 128000)
|
||||
controlKey = key.NewMachine()
|
||||
machineKey = key.NewMachine()
|
||||
clientErr = make(chan error, 2)
|
||||
clientBytes = make(chan []byte, 1)
|
||||
c2s = "client>server"
|
||||
s2c = "server>client"
|
||||
)
|
||||
|
||||
go func() {
|
||||
client, err := Client(context.Background(), s1, machineKey, controlKey.Public())
|
||||
clientErr <- err
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = client.Write([]byte(c2s))
|
||||
if err != nil {
|
||||
clientErr <- err
|
||||
return
|
||||
}
|
||||
var buf [1024]byte
|
||||
_, err = io.ReadFull(client, buf[:len(s2c)])
|
||||
clientBytes <- buf[:len(s2c)]
|
||||
clientErr <- err
|
||||
}()
|
||||
|
||||
gotC2S, err := noiseExplorerServer(s2, controlKey, machineKey.Public(), []byte(s2c))
|
||||
if err != nil {
|
||||
t.Fatalf("failed server interop: %v", err)
|
||||
}
|
||||
if string(gotC2S) != c2s {
|
||||
t.Fatalf("server sent unexpected data %q, want %q", string(gotC2S), c2s)
|
||||
}
|
||||
|
||||
if err := <-clientErr; err != nil {
|
||||
t.Fatalf("client handshake failed: %v", err)
|
||||
}
|
||||
if err := <-clientErr; err != nil {
|
||||
t.Fatalf("client read/write failed: %v", err)
|
||||
}
|
||||
if got := string(<-clientBytes); got != s2c {
|
||||
t.Fatalf("client received %q, want %q", got, s2c)
|
||||
}
|
||||
}
|
||||
|
||||
// noiseExplorerClient uses the Noise Explorer implementation of Noise
|
||||
// IK to handshake as a Noise client on conn, transmit payload, and
|
||||
// read+return a payload from the peer.
|
||||
func noiseExplorerClient(conn net.Conn, controlKey key.MachinePublic, machineKey key.MachinePrivate, payload []byte) ([]byte, error) {
|
||||
var mk keypair
|
||||
copy(mk.private_key[:], machineKey.UntypedBytes())
|
||||
copy(mk.public_key[:], machineKey.Public().UntypedBytes())
|
||||
var peerKey [32]byte
|
||||
copy(peerKey[:], controlKey.UntypedBytes())
|
||||
session := InitSession(true, protocolVersionPrologue(protocolVersion), mk, peerKey)
|
||||
|
||||
_, msg1 := SendMessage(&session, nil)
|
||||
var hdr [initiationHeaderLen]byte
|
||||
binary.BigEndian.PutUint16(hdr[:2], protocolVersion)
|
||||
hdr[2] = msgTypeInitiation
|
||||
binary.BigEndian.PutUint16(hdr[3:5], 96)
|
||||
if _, err := conn.Write(hdr[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(msg1.ne[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(msg1.ns); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(msg1.ciphertext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf [1024]byte
|
||||
if _, err := io.ReadFull(conn, buf[:51]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ignore the header for this test, we're only checking the noise
|
||||
// implementation.
|
||||
msg2 := messagebuffer{
|
||||
ciphertext: buf[35:51],
|
||||
}
|
||||
copy(msg2.ne[:], buf[3:35])
|
||||
_, p, valid := RecvMessage(&session, &msg2)
|
||||
if !valid {
|
||||
return nil, errors.New("handshake failed")
|
||||
}
|
||||
if len(p) != 0 {
|
||||
return nil, errors.New("non-empty payload")
|
||||
}
|
||||
|
||||
_, msg3 := SendMessage(&session, payload)
|
||||
hdr[0] = msgTypeRecord
|
||||
binary.BigEndian.PutUint16(hdr[1:3], uint16(len(msg3.ciphertext)))
|
||||
if _, err := conn.Write(hdr[:3]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(msg3.ciphertext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:3]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Ignore all of the header except the payload length
|
||||
plen := int(binary.BigEndian.Uint16(buf[1:3]))
|
||||
if _, err := io.ReadFull(conn, buf[:plen]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg4 := messagebuffer{
|
||||
ciphertext: buf[:plen],
|
||||
}
|
||||
_, p, valid = RecvMessage(&session, &msg4)
|
||||
if !valid {
|
||||
return nil, errors.New("transport message decryption failed")
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func noiseExplorerServer(conn net.Conn, controlKey key.MachinePrivate, wantMachineKey key.MachinePublic, payload []byte) ([]byte, error) {
|
||||
var mk keypair
|
||||
copy(mk.private_key[:], controlKey.UntypedBytes())
|
||||
copy(mk.public_key[:], controlKey.Public().UntypedBytes())
|
||||
session := InitSession(false, protocolVersionPrologue(protocolVersion), mk, [32]byte{})
|
||||
|
||||
var buf [1024]byte
|
||||
if _, err := io.ReadFull(conn, buf[:101]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Ignore the header, we're just checking the noise implementation.
|
||||
msg1 := messagebuffer{
|
||||
ns: buf[37:85],
|
||||
ciphertext: buf[85:101],
|
||||
}
|
||||
copy(msg1.ne[:], buf[5:37])
|
||||
_, p, valid := RecvMessage(&session, &msg1)
|
||||
if !valid {
|
||||
return nil, errors.New("handshake failed")
|
||||
}
|
||||
if len(p) != 0 {
|
||||
return nil, errors.New("non-empty payload")
|
||||
}
|
||||
|
||||
_, msg2 := SendMessage(&session, nil)
|
||||
var hdr [headerLen]byte
|
||||
hdr[0] = msgTypeResponse
|
||||
binary.BigEndian.PutUint16(hdr[1:3], 48)
|
||||
if _, err := conn.Write(hdr[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(msg2.ne[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(msg2.ciphertext[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:3]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plen := int(binary.BigEndian.Uint16(buf[1:3]))
|
||||
if _, err := io.ReadFull(conn, buf[:plen]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg3 := messagebuffer{
|
||||
ciphertext: buf[:plen],
|
||||
}
|
||||
_, p, valid = RecvMessage(&session, &msg3)
|
||||
if !valid {
|
||||
return nil, errors.New("transport message decryption failed")
|
||||
}
|
||||
|
||||
_, msg4 := SendMessage(&session, payload)
|
||||
hdr[0] = msgTypeRecord
|
||||
binary.BigEndian.PutUint16(hdr[1:3], uint16(len(msg4.ciphertext)))
|
||||
if _, err := conn.Write(hdr[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(msg4.ciphertext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
88
control/noise/messages.go
Normal file
88
control/noise/messages.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// 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 noise
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
const (
|
||||
// msgTypeInitiation frames carry a Noise IK handshake initiation message.
|
||||
msgTypeInitiation = 1
|
||||
// msgTypeResponse frames carry a Noise IK handshake response message.
|
||||
msgTypeResponse = 2
|
||||
// msgTypeError frames carry an unauthenticated human-readable
|
||||
// error message.
|
||||
//
|
||||
// Errors reported in this message type must be treated as public
|
||||
// hints only. They are not encrypted or authenticated, and so can
|
||||
// be seen and tampered with on the wire.
|
||||
msgTypeError = 3
|
||||
// msgTypeRecord frames carry session data bytes.
|
||||
msgTypeRecord = 4
|
||||
|
||||
// headerLen is the size of the header on all messages except msgTypeInitiation.
|
||||
headerLen = 3
|
||||
// initiationHeaderLen is the size of the header on all msgTypeInitiation messages.
|
||||
initiationHeaderLen = 5
|
||||
)
|
||||
|
||||
// initiationMessage is the protocol message sent from a client
|
||||
// machine to a control server.
|
||||
//
|
||||
// 2b: protocol version
|
||||
// 1b: message type (0x01)
|
||||
// 2b: payload length (96)
|
||||
// 5b: header (see headerLen for fields)
|
||||
// 32b: client ephemeral public key (cleartext)
|
||||
// 48b: client machine public key (encrypted)
|
||||
// 16b: message tag (authenticates the whole message)
|
||||
type initiationMessage [101]byte
|
||||
|
||||
func mkInitiationMessage() initiationMessage {
|
||||
var ret initiationMessage
|
||||
binary.BigEndian.PutUint16(ret[:2], uint16(protocolVersion))
|
||||
ret[2] = msgTypeInitiation
|
||||
binary.BigEndian.PutUint16(ret[3:5], uint16(len(ret.Payload())))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *initiationMessage) Header() []byte { return m[:initiationHeaderLen] }
|
||||
func (m *initiationMessage) Payload() []byte { return m[initiationHeaderLen:] }
|
||||
|
||||
func (m *initiationMessage) Version() uint16 { return binary.BigEndian.Uint16(m[:2]) }
|
||||
func (m *initiationMessage) Type() byte { return m[2] }
|
||||
func (m *initiationMessage) Length() int { return int(binary.BigEndian.Uint16(m[3:5])) }
|
||||
|
||||
func (m *initiationMessage) EphemeralPub() []byte {
|
||||
return m[initiationHeaderLen : initiationHeaderLen+32]
|
||||
}
|
||||
func (m *initiationMessage) MachinePub() []byte {
|
||||
return m[initiationHeaderLen+32 : initiationHeaderLen+32+48]
|
||||
}
|
||||
func (m *initiationMessage) Tag() []byte { return m[initiationHeaderLen+32+48:] }
|
||||
|
||||
// responseMessage is the protocol message sent from a control server
|
||||
// to a client machine.
|
||||
//
|
||||
// 1b: message type (0x02)
|
||||
// 2b: payload length (48)
|
||||
// 32b: control ephemeral public key (cleartext)
|
||||
// 16b: message tag (authenticates the whole message)
|
||||
type responseMessage [51]byte
|
||||
|
||||
func mkResponseMessage() responseMessage {
|
||||
var ret responseMessage
|
||||
ret[0] = msgTypeResponse
|
||||
binary.BigEndian.PutUint16(ret[1:], uint16(len(ret.Payload())))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *responseMessage) Header() []byte { return m[:headerLen] }
|
||||
func (m *responseMessage) Payload() []byte { return m[headerLen:] }
|
||||
|
||||
func (m *responseMessage) Type() byte { return m[0] }
|
||||
func (m *responseMessage) Length() int { return int(binary.BigEndian.Uint16(m[1:3])) }
|
||||
|
||||
func (m *responseMessage) EphemeralPub() []byte { return m[headerLen : headerLen+32] }
|
||||
func (m *responseMessage) Tag() []byte { return m[headerLen+32:] }
|
||||
475
control/noise/noiseexplorer_test.go
Normal file
475
control/noise/noiseexplorer_test.go
Normal file
@@ -0,0 +1,475 @@
|
||||
// This file contains the implementation of Noise IK from
|
||||
// https://noiseexplorer.com/ . Unlike the rest of this repository,
|
||||
// this file is licensed under the terms of the GNU GPL v3. See
|
||||
// https://source.symbolic.software/noiseexplorer/noiseexplorer for
|
||||
// more information.
|
||||
//
|
||||
// This file is used here to verify that Tailscale's implementation of
|
||||
// Noise IK is interoperable with another implementation.
|
||||
//lint:file-ignore SA4006 not our code.
|
||||
|
||||
/*
|
||||
IK:
|
||||
<- s
|
||||
...
|
||||
-> e, es, s, ss
|
||||
<- e, ee, se
|
||||
->
|
||||
<-
|
||||
*/
|
||||
|
||||
// Implementation Version: 1.0.2
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* PARAMETERS *
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
package noise
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"golang.org/x/crypto/blake2s"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* TYPES *
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
type keypair struct {
|
||||
public_key [32]byte
|
||||
private_key [32]byte
|
||||
}
|
||||
|
||||
type messagebuffer struct {
|
||||
ne [32]byte
|
||||
ns []byte
|
||||
ciphertext []byte
|
||||
}
|
||||
|
||||
type cipherstate struct {
|
||||
k [32]byte
|
||||
n uint32
|
||||
}
|
||||
|
||||
type symmetricstate struct {
|
||||
cs cipherstate
|
||||
ck [32]byte
|
||||
h [32]byte
|
||||
}
|
||||
|
||||
type handshakestate struct {
|
||||
ss symmetricstate
|
||||
s keypair
|
||||
e keypair
|
||||
rs [32]byte
|
||||
re [32]byte
|
||||
psk [32]byte
|
||||
}
|
||||
|
||||
type noisesession struct {
|
||||
hs handshakestate
|
||||
h [32]byte
|
||||
cs1 cipherstate
|
||||
cs2 cipherstate
|
||||
mc uint64
|
||||
i bool
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* CONSTANTS *
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
var emptyKey = [32]byte{
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
var minNonce = uint32(0)
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* UTILITY FUNCTIONS *
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
func getPublicKey(kp *keypair) [32]byte {
|
||||
return kp.public_key
|
||||
}
|
||||
|
||||
func isEmptyKey(k [32]byte) bool {
|
||||
return subtle.ConstantTimeCompare(k[:], emptyKey[:]) == 1
|
||||
}
|
||||
|
||||
func validatePublicKey(k []byte) bool {
|
||||
forbiddenCurveValues := [12][]byte{
|
||||
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
{224, 235, 122, 124, 59, 65, 184, 174, 22, 86, 227, 250, 241, 159, 196, 106, 218, 9, 141, 235, 156, 50, 177, 253, 134, 98, 5, 22, 95, 73, 184, 0},
|
||||
{95, 156, 149, 188, 163, 80, 140, 36, 177, 208, 177, 85, 156, 131, 239, 91, 4, 68, 92, 196, 88, 28, 142, 134, 216, 34, 78, 221, 208, 159, 17, 87},
|
||||
{236, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127},
|
||||
{237, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127},
|
||||
{238, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127},
|
||||
{205, 235, 122, 124, 59, 65, 184, 174, 22, 86, 227, 250, 241, 159, 196, 106, 218, 9, 141, 235, 156, 50, 177, 253, 134, 98, 5, 22, 95, 73, 184, 128},
|
||||
{76, 156, 149, 188, 163, 80, 140, 36, 177, 208, 177, 85, 156, 131, 239, 91, 4, 68, 92, 196, 88, 28, 142, 134, 216, 34, 78, 221, 208, 159, 17, 215},
|
||||
{217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
|
||||
{218, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
|
||||
{219, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 25},
|
||||
}
|
||||
|
||||
for _, testValue := range forbiddenCurveValues {
|
||||
if subtle.ConstantTimeCompare(k[:], testValue[:]) == 1 {
|
||||
panic("Invalid public key")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* PRIMITIVES *
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
func incrementNonce(n uint32) uint32 {
|
||||
return n + 1
|
||||
}
|
||||
|
||||
func dh(private_key [32]byte, public_key [32]byte) [32]byte {
|
||||
var ss [32]byte
|
||||
curve25519.ScalarMult(&ss, &private_key, &public_key)
|
||||
return ss
|
||||
}
|
||||
|
||||
func generateKeypair() keypair {
|
||||
var public_key [32]byte
|
||||
var private_key [32]byte
|
||||
_, _ = rand.Read(private_key[:])
|
||||
curve25519.ScalarBaseMult(&public_key, &private_key)
|
||||
if validatePublicKey(public_key[:]) {
|
||||
return keypair{public_key, private_key}
|
||||
}
|
||||
return generateKeypair()
|
||||
}
|
||||
|
||||
func generatePublicKey(private_key [32]byte) [32]byte {
|
||||
var public_key [32]byte
|
||||
curve25519.ScalarBaseMult(&public_key, &private_key)
|
||||
return public_key
|
||||
}
|
||||
|
||||
func encrypt(k [32]byte, n uint32, ad []byte, plaintext []byte) []byte {
|
||||
var nonce [12]byte
|
||||
var ciphertext []byte
|
||||
enc, _ := chacha20poly1305.New(k[:])
|
||||
binary.LittleEndian.PutUint32(nonce[4:], n)
|
||||
ciphertext = enc.Seal(nil, nonce[:], plaintext, ad)
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
func decrypt(k [32]byte, n uint32, ad []byte, ciphertext []byte) (bool, []byte, []byte) {
|
||||
var nonce [12]byte
|
||||
var plaintext []byte
|
||||
enc, err := chacha20poly1305.New(k[:])
|
||||
binary.LittleEndian.PutUint32(nonce[4:], n)
|
||||
plaintext, err = enc.Open(nil, nonce[:], ciphertext, ad)
|
||||
return (err == nil), ad, plaintext
|
||||
}
|
||||
|
||||
func getHash(a []byte, b []byte) [32]byte {
|
||||
return blake2s.Sum256(append(a, b...))
|
||||
}
|
||||
|
||||
func hashProtocolName(protocolName []byte) [32]byte {
|
||||
var h [32]byte
|
||||
if len(protocolName) <= 32 {
|
||||
copy(h[:], protocolName)
|
||||
} else {
|
||||
h = getHash(protocolName, []byte{})
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func blake2HkdfInterface() hash.Hash {
|
||||
h, _ := blake2s.New256([]byte{})
|
||||
return h
|
||||
}
|
||||
|
||||
func getHkdf(ck [32]byte, ikm []byte) ([32]byte, [32]byte, [32]byte) {
|
||||
var k1 [32]byte
|
||||
var k2 [32]byte
|
||||
var k3 [32]byte
|
||||
output := hkdf.New(blake2HkdfInterface, ikm[:], ck[:], []byte{})
|
||||
io.ReadFull(output, k1[:])
|
||||
io.ReadFull(output, k2[:])
|
||||
io.ReadFull(output, k3[:])
|
||||
return k1, k2, k3
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* STATE MANAGEMENT *
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/* CipherState */
|
||||
func initializeKey(k [32]byte) cipherstate {
|
||||
return cipherstate{k, minNonce}
|
||||
}
|
||||
|
||||
func hasKey(cs *cipherstate) bool {
|
||||
return !isEmptyKey(cs.k)
|
||||
}
|
||||
|
||||
func setNonce(cs *cipherstate, newNonce uint32) *cipherstate {
|
||||
cs.n = newNonce
|
||||
return cs
|
||||
}
|
||||
|
||||
func encryptWithAd(cs *cipherstate, ad []byte, plaintext []byte) (*cipherstate, []byte) {
|
||||
e := encrypt(cs.k, cs.n, ad, plaintext)
|
||||
cs = setNonce(cs, incrementNonce(cs.n))
|
||||
return cs, e
|
||||
}
|
||||
|
||||
func decryptWithAd(cs *cipherstate, ad []byte, ciphertext []byte) (*cipherstate, []byte, bool) {
|
||||
valid, ad, plaintext := decrypt(cs.k, cs.n, ad, ciphertext)
|
||||
cs = setNonce(cs, incrementNonce(cs.n))
|
||||
return cs, plaintext, valid
|
||||
}
|
||||
|
||||
func reKey(cs *cipherstate) *cipherstate {
|
||||
e := encrypt(cs.k, math.MaxUint32, []byte{}, emptyKey[:])
|
||||
copy(cs.k[:], e)
|
||||
return cs
|
||||
}
|
||||
|
||||
/* SymmetricState */
|
||||
|
||||
func initializeSymmetric(protocolName []byte) symmetricstate {
|
||||
h := hashProtocolName(protocolName)
|
||||
ck := h
|
||||
cs := initializeKey(emptyKey)
|
||||
return symmetricstate{cs, ck, h}
|
||||
}
|
||||
|
||||
func mixKey(ss *symmetricstate, ikm [32]byte) *symmetricstate {
|
||||
ck, tempK, _ := getHkdf(ss.ck, ikm[:])
|
||||
ss.cs = initializeKey(tempK)
|
||||
ss.ck = ck
|
||||
return ss
|
||||
}
|
||||
|
||||
func mixHash(ss *symmetricstate, data []byte) *symmetricstate {
|
||||
ss.h = getHash(ss.h[:], data)
|
||||
return ss
|
||||
}
|
||||
|
||||
func mixKeyAndHash(ss *symmetricstate, ikm [32]byte) *symmetricstate {
|
||||
var tempH [32]byte
|
||||
var tempK [32]byte
|
||||
ss.ck, tempH, tempK = getHkdf(ss.ck, ikm[:])
|
||||
ss = mixHash(ss, tempH[:])
|
||||
ss.cs = initializeKey(tempK)
|
||||
return ss
|
||||
}
|
||||
|
||||
func getHandshakeHash(ss *symmetricstate) [32]byte {
|
||||
return ss.h
|
||||
}
|
||||
|
||||
func encryptAndHash(ss *symmetricstate, plaintext []byte) (*symmetricstate, []byte) {
|
||||
var ciphertext []byte
|
||||
if hasKey(&ss.cs) {
|
||||
_, ciphertext = encryptWithAd(&ss.cs, ss.h[:], plaintext)
|
||||
} else {
|
||||
ciphertext = plaintext
|
||||
}
|
||||
ss = mixHash(ss, ciphertext)
|
||||
return ss, ciphertext
|
||||
}
|
||||
|
||||
func decryptAndHash(ss *symmetricstate, ciphertext []byte) (*symmetricstate, []byte, bool) {
|
||||
var plaintext []byte
|
||||
var valid bool
|
||||
if hasKey(&ss.cs) {
|
||||
_, plaintext, valid = decryptWithAd(&ss.cs, ss.h[:], ciphertext)
|
||||
} else {
|
||||
plaintext, valid = ciphertext, true
|
||||
}
|
||||
ss = mixHash(ss, ciphertext)
|
||||
return ss, plaintext, valid
|
||||
}
|
||||
|
||||
func split(ss *symmetricstate) (cipherstate, cipherstate) {
|
||||
tempK1, tempK2, _ := getHkdf(ss.ck, []byte{})
|
||||
cs1 := initializeKey(tempK1)
|
||||
cs2 := initializeKey(tempK2)
|
||||
return cs1, cs2
|
||||
}
|
||||
|
||||
/* HandshakeState */
|
||||
|
||||
func initializeInitiator(prologue []byte, s keypair, rs [32]byte, psk [32]byte) handshakestate {
|
||||
var ss symmetricstate
|
||||
var e keypair
|
||||
var re [32]byte
|
||||
name := []byte("Noise_IK_25519_ChaChaPoly_BLAKE2s")
|
||||
ss = initializeSymmetric(name)
|
||||
mixHash(&ss, prologue)
|
||||
mixHash(&ss, rs[:])
|
||||
return handshakestate{ss, s, e, rs, re, psk}
|
||||
}
|
||||
|
||||
func initializeResponder(prologue []byte, s keypair, rs [32]byte, psk [32]byte) handshakestate {
|
||||
var ss symmetricstate
|
||||
var e keypair
|
||||
var re [32]byte
|
||||
name := []byte("Noise_IK_25519_ChaChaPoly_BLAKE2s")
|
||||
ss = initializeSymmetric(name)
|
||||
mixHash(&ss, prologue)
|
||||
mixHash(&ss, s.public_key[:])
|
||||
return handshakestate{ss, s, e, rs, re, psk}
|
||||
}
|
||||
|
||||
func writeMessageA(hs *handshakestate, payload []byte) (*handshakestate, messagebuffer) {
|
||||
ne, ns, ciphertext := emptyKey, []byte{}, []byte{}
|
||||
hs.e = generateKeypair()
|
||||
ne = hs.e.public_key
|
||||
mixHash(&hs.ss, ne[:])
|
||||
/* No PSK, so skipping mixKey */
|
||||
mixKey(&hs.ss, dh(hs.e.private_key, hs.rs))
|
||||
spk := make([]byte, len(hs.s.public_key))
|
||||
copy(spk[:], hs.s.public_key[:])
|
||||
_, ns = encryptAndHash(&hs.ss, spk)
|
||||
mixKey(&hs.ss, dh(hs.s.private_key, hs.rs))
|
||||
_, ciphertext = encryptAndHash(&hs.ss, payload)
|
||||
messageBuffer := messagebuffer{ne, ns, ciphertext}
|
||||
return hs, messageBuffer
|
||||
}
|
||||
|
||||
func writeMessageB(hs *handshakestate, payload []byte) ([32]byte, messagebuffer, cipherstate, cipherstate) {
|
||||
ne, ns, ciphertext := emptyKey, []byte{}, []byte{}
|
||||
hs.e = generateKeypair()
|
||||
ne = hs.e.public_key
|
||||
mixHash(&hs.ss, ne[:])
|
||||
/* No PSK, so skipping mixKey */
|
||||
mixKey(&hs.ss, dh(hs.e.private_key, hs.re))
|
||||
mixKey(&hs.ss, dh(hs.e.private_key, hs.rs))
|
||||
_, ciphertext = encryptAndHash(&hs.ss, payload)
|
||||
messageBuffer := messagebuffer{ne, ns, ciphertext}
|
||||
cs1, cs2 := split(&hs.ss)
|
||||
return hs.ss.h, messageBuffer, cs1, cs2
|
||||
}
|
||||
|
||||
func writeMessageRegular(cs *cipherstate, payload []byte) (*cipherstate, messagebuffer) {
|
||||
ne, ns, ciphertext := emptyKey, []byte{}, []byte{}
|
||||
cs, ciphertext = encryptWithAd(cs, []byte{}, payload)
|
||||
messageBuffer := messagebuffer{ne, ns, ciphertext}
|
||||
return cs, messageBuffer
|
||||
}
|
||||
|
||||
func readMessageA(hs *handshakestate, message *messagebuffer) (*handshakestate, []byte, bool) {
|
||||
valid1 := true
|
||||
if validatePublicKey(message.ne[:]) {
|
||||
hs.re = message.ne
|
||||
}
|
||||
mixHash(&hs.ss, hs.re[:])
|
||||
/* No PSK, so skipping mixKey */
|
||||
mixKey(&hs.ss, dh(hs.s.private_key, hs.re))
|
||||
_, ns, valid1 := decryptAndHash(&hs.ss, message.ns)
|
||||
if valid1 && len(ns) == 32 && validatePublicKey(message.ns[:]) {
|
||||
copy(hs.rs[:], ns)
|
||||
}
|
||||
mixKey(&hs.ss, dh(hs.s.private_key, hs.rs))
|
||||
_, plaintext, valid2 := decryptAndHash(&hs.ss, message.ciphertext)
|
||||
return hs, plaintext, (valid1 && valid2)
|
||||
}
|
||||
|
||||
func readMessageB(hs *handshakestate, message *messagebuffer) ([32]byte, []byte, bool, cipherstate, cipherstate) {
|
||||
valid1 := true
|
||||
if validatePublicKey(message.ne[:]) {
|
||||
hs.re = message.ne
|
||||
}
|
||||
mixHash(&hs.ss, hs.re[:])
|
||||
/* No PSK, so skipping mixKey */
|
||||
mixKey(&hs.ss, dh(hs.e.private_key, hs.re))
|
||||
mixKey(&hs.ss, dh(hs.s.private_key, hs.re))
|
||||
_, plaintext, valid2 := decryptAndHash(&hs.ss, message.ciphertext)
|
||||
cs1, cs2 := split(&hs.ss)
|
||||
return hs.ss.h, plaintext, (valid1 && valid2), cs1, cs2
|
||||
}
|
||||
|
||||
func readMessageRegular(cs *cipherstate, message *messagebuffer) (*cipherstate, []byte, bool) {
|
||||
/* No encrypted keys */
|
||||
_, plaintext, valid2 := decryptWithAd(cs, []byte{}, message.ciphertext)
|
||||
return cs, plaintext, valid2
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* PROCESSES *
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
func InitSession(initiator bool, prologue []byte, s keypair, rs [32]byte) noisesession {
|
||||
var session noisesession
|
||||
psk := emptyKey
|
||||
if initiator {
|
||||
session.hs = initializeInitiator(prologue, s, rs, psk)
|
||||
} else {
|
||||
session.hs = initializeResponder(prologue, s, rs, psk)
|
||||
}
|
||||
session.i = initiator
|
||||
session.mc = 0
|
||||
return session
|
||||
}
|
||||
|
||||
func SendMessage(session *noisesession, message []byte) (*noisesession, messagebuffer) {
|
||||
var messageBuffer messagebuffer
|
||||
if session.mc == 0 {
|
||||
_, messageBuffer = writeMessageA(&session.hs, message)
|
||||
}
|
||||
if session.mc == 1 {
|
||||
session.h, messageBuffer, session.cs1, session.cs2 = writeMessageB(&session.hs, message)
|
||||
session.hs = handshakestate{}
|
||||
}
|
||||
if session.mc > 1 {
|
||||
if session.i {
|
||||
_, messageBuffer = writeMessageRegular(&session.cs1, message)
|
||||
} else {
|
||||
_, messageBuffer = writeMessageRegular(&session.cs2, message)
|
||||
}
|
||||
}
|
||||
session.mc = session.mc + 1
|
||||
return session, messageBuffer
|
||||
}
|
||||
|
||||
func RecvMessage(session *noisesession, message *messagebuffer) (*noisesession, []byte, bool) {
|
||||
var plaintext []byte
|
||||
var valid bool
|
||||
if session.mc == 0 {
|
||||
_, plaintext, valid = readMessageA(&session.hs, message)
|
||||
}
|
||||
if session.mc == 1 {
|
||||
session.h, plaintext, valid, session.cs1, session.cs2 = readMessageB(&session.hs, message)
|
||||
session.hs = handshakestate{}
|
||||
}
|
||||
if session.mc > 1 {
|
||||
if session.i {
|
||||
_, plaintext, valid = readMessageRegular(&session.cs2, message)
|
||||
} else {
|
||||
_, plaintext, valid = readMessageRegular(&session.cs1, message)
|
||||
}
|
||||
}
|
||||
session.mc = session.mc + 1
|
||||
return session, plaintext, valid
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@@ -6,7 +6,6 @@ package derp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -15,7 +14,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -23,9 +22,9 @@ import (
|
||||
|
||||
// Client is a DERP client.
|
||||
type Client struct {
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
serverKey key.NodePublic // of the DERP server; not a machine or node key
|
||||
privateKey key.NodePrivate
|
||||
publicKey key.NodePublic // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
@@ -54,7 +53,7 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
ServerPub key.NodePublic
|
||||
CanAckPings bool
|
||||
IsProber bool
|
||||
}
|
||||
@@ -71,7 +70,7 @@ func IsProber(v bool) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.Is
|
||||
|
||||
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
|
||||
// If key is the zero value, the returned ClientOpt is a no-op.
|
||||
func ServerPublicKey(key key.Public) ClientOpt {
|
||||
func ServerPublicKey(key key.NodePublic) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
|
||||
}
|
||||
|
||||
@@ -81,7 +80,7 @@ func CanAckPings(v bool) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.CanAckPings = v })
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
func NewClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
var opt clientOpt
|
||||
for _, o := range opts {
|
||||
if o == nil {
|
||||
@@ -92,7 +91,7 @@ func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
return newClient(privateKey, nc, brw, logf, opt)
|
||||
}
|
||||
|
||||
func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
|
||||
func newClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
@@ -130,7 +129,7 @@ func (c *Client) recvServerKey() error {
|
||||
if flen < uint32(len(buf)) || t != frameServerKey || string(buf[:len(magic)]) != magic {
|
||||
return errors.New("invalid server greeting")
|
||||
}
|
||||
copy(c.serverKey[:], buf[len(magic):])
|
||||
c.serverKey = key.NodePublicFromRaw32(mem.B(buf[len(magic):]))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -143,13 +142,9 @@ func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
if fl > maxLength {
|
||||
return nil, fmt.Errorf("long serverInfo frame")
|
||||
}
|
||||
// TODO: add a read-nonce-and-box helper
|
||||
var nonce [nonceLen]byte
|
||||
copy(nonce[:], b)
|
||||
msgbox := b[nonceLen:]
|
||||
msg, ok := box.Open(nil, msgbox, &nonce, c.serverKey.B32(), c.privateKey.B32())
|
||||
msg, ok := c.privateKey.OpenFrom(c.serverKey, b)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to open naclbox from server key %x", c.serverKey[:])
|
||||
return nil, fmt.Errorf("failed to open naclbox from server key %s", c.serverKey)
|
||||
}
|
||||
info := new(serverInfo)
|
||||
if err := json.Unmarshal(msg, info); err != nil {
|
||||
@@ -176,10 +171,6 @@ type clientInfo struct {
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
var nonce [nonceLen]byte
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
@@ -189,24 +180,23 @@ func (c *Client) sendClientKey() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgbox := box.Seal(nil, msg, &nonce, c.serverKey.B32(), c.privateKey.B32())
|
||||
msgbox := c.privateKey.SealTo(c.serverKey, msg)
|
||||
|
||||
buf := make([]byte, 0, nonceLen+keyLen+len(msgbox))
|
||||
buf = append(buf, c.publicKey[:]...)
|
||||
buf = append(buf, nonce[:]...)
|
||||
buf := make([]byte, 0, keyLen+len(msgbox))
|
||||
buf = c.publicKey.AppendTo(buf)
|
||||
buf = append(buf, msgbox...)
|
||||
return writeFrame(c.bw, frameClientInfo, buf)
|
||||
}
|
||||
|
||||
// ServerPublicKey returns the server's public key.
|
||||
func (c *Client) ServerPublicKey() key.Public { return c.serverKey }
|
||||
func (c *Client) ServerPublicKey() key.NodePublic { return c.serverKey }
|
||||
|
||||
// Send sends a packet to the Tailscale node identified by dstKey.
|
||||
//
|
||||
// It is an error if the packet is larger than 64KB.
|
||||
func (c *Client) Send(dstKey key.Public, pkt []byte) error { return c.send(dstKey, pkt) }
|
||||
func (c *Client) Send(dstKey key.NodePublic, pkt []byte) error { return c.send(dstKey, pkt) }
|
||||
|
||||
func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) {
|
||||
func (c *Client) send(dstKey key.NodePublic, pkt []byte) (ret error) {
|
||||
defer func() {
|
||||
if ret != nil {
|
||||
ret = fmt.Errorf("derp.Send: %w", ret)
|
||||
@@ -220,15 +210,15 @@ func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if c.rate != nil {
|
||||
pktLen := frameHeaderLen + len(dstKey) + len(pkt)
|
||||
pktLen := frameHeaderLen + key.NodePublicRawLen + len(pkt)
|
||||
if !c.rate.AllowN(time.Now(), pktLen) {
|
||||
return nil // drop
|
||||
}
|
||||
}
|
||||
if err := writeFrameHeader(c.bw, frameSendPacket, uint32(len(dstKey)+len(pkt))); err != nil {
|
||||
if err := writeFrameHeader(c.bw, frameSendPacket, uint32(key.NodePublicRawLen+len(pkt))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(dstKey[:]); err != nil {
|
||||
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(pkt); err != nil {
|
||||
@@ -237,7 +227,7 @@ func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) {
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
func (c *Client) ForwardPacket(srcKey, dstKey key.Public, pkt []byte) (err error) {
|
||||
func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("derp.ForwardPacket: %w", err)
|
||||
@@ -257,10 +247,10 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.Public, pkt []byte) (err error
|
||||
if err := writeFrameHeader(c.bw, frameForwardPacket, uint32(keyLen*2+len(pkt))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(srcKey[:]); err != nil {
|
||||
if _, err := c.bw.Write(srcKey.AppendTo(nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(dstKey[:]); err != nil {
|
||||
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(pkt); err != nil {
|
||||
@@ -322,10 +312,10 @@ func (c *Client) WatchConnectionChanges() error {
|
||||
|
||||
// ClosePeer asks the server to close target's TCP connection.
|
||||
// It's a fatal error if the client wasn't created using MeshKey.
|
||||
func (c *Client) ClosePeer(target key.Public) error {
|
||||
func (c *Client) ClosePeer(target key.NodePublic) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
return writeFrame(c.bw, frameClosePeer, target[:])
|
||||
return writeFrame(c.bw, frameClosePeer, target.AppendTo(nil))
|
||||
}
|
||||
|
||||
// ReceivedMessage represents a type returned by Client.Recv. Unless
|
||||
@@ -338,7 +328,7 @@ type ReceivedMessage interface {
|
||||
|
||||
// ReceivedPacket is a ReceivedMessage representing an incoming packet.
|
||||
type ReceivedPacket struct {
|
||||
Source key.Public
|
||||
Source key.NodePublic
|
||||
// Data is the received packet bytes. It aliases the memory
|
||||
// passed to Client.Recv.
|
||||
Data []byte
|
||||
@@ -349,13 +339,13 @@ func (ReceivedPacket) msg() {}
|
||||
// PeerGoneMessage is a ReceivedMessage that indicates that the client
|
||||
// identified by the underlying public key had previously sent you a
|
||||
// packet but has now disconnected from the server.
|
||||
type PeerGoneMessage key.Public
|
||||
type PeerGoneMessage key.NodePublic
|
||||
|
||||
func (PeerGoneMessage) msg() {}
|
||||
|
||||
// PeerPresentMessage is a ReceivedMessage that indicates that the client
|
||||
// is connected to the server. (Only used by trusted mesh clients)
|
||||
type PeerPresentMessage key.Public
|
||||
type PeerPresentMessage key.NodePublic
|
||||
|
||||
func (PeerPresentMessage) msg() {}
|
||||
|
||||
@@ -516,8 +506,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
c.logf("[unexpected] dropping short peerGone frame from DERP server")
|
||||
continue
|
||||
}
|
||||
var pg PeerGoneMessage
|
||||
copy(pg[:], b[:keyLen])
|
||||
pg := PeerGoneMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
|
||||
return pg, nil
|
||||
|
||||
case framePeerPresent:
|
||||
@@ -525,8 +514,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
c.logf("[unexpected] dropping short peerPresent frame from DERP server")
|
||||
continue
|
||||
}
|
||||
var pg PeerPresentMessage
|
||||
copy(pg[:], b[:keyLen])
|
||||
pg := PeerPresentMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
|
||||
return pg, nil
|
||||
|
||||
case frameRecvPacket:
|
||||
@@ -535,7 +523,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
}
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Source = key.NodePublicFromRaw32(mem.B(b[:keyLen]))
|
||||
rp.Data = b[keyLen:n]
|
||||
return rp, nil
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import (
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
@@ -52,7 +51,7 @@ var debug, _ = strconv.ParseBool(os.Getenv("DERP_DEBUG_LOGS"))
|
||||
|
||||
// verboseDropKeys is the set of destination public keys that should
|
||||
// verbosely log whenever DERP drops a packet.
|
||||
var verboseDropKeys = map[key.Public]bool{}
|
||||
var verboseDropKeys = map[key.NodePublic]bool{}
|
||||
|
||||
func init() {
|
||||
keys := os.Getenv("TS_DEBUG_VERBOSE_DROPS")
|
||||
@@ -60,7 +59,7 @@ func init() {
|
||||
return
|
||||
}
|
||||
for _, keyStr := range strings.Split(keys, ",") {
|
||||
k, err := key.NewPublicFromHexMem(mem.S(keyStr))
|
||||
k, err := key.ParseNodePublicUntyped(mem.S(keyStr))
|
||||
if err != nil {
|
||||
log.Printf("ignoring invalid debug key %q: %v", keyStr, err)
|
||||
} else {
|
||||
@@ -99,8 +98,8 @@ type Server struct {
|
||||
// before failing when writing to a client.
|
||||
WriteTimeout time.Duration
|
||||
|
||||
privateKey key.Private
|
||||
publicKey key.Public
|
||||
privateKey key.NodePrivate
|
||||
publicKey key.NodePublic
|
||||
logf logger.Logf
|
||||
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
|
||||
meshKey string
|
||||
@@ -146,22 +145,22 @@ type Server struct {
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
netConns map[Conn]chan struct{} // chan is closed when conn closes
|
||||
clients map[key.Public]clientSet
|
||||
clients map[key.NodePublic]clientSet
|
||||
watchers map[*sclient]bool // mesh peer -> true
|
||||
// clientsMesh tracks all clients in the cluster, both locally
|
||||
// and to mesh peers. If the value is nil, that means the
|
||||
// peer is only local (and thus in the clients Map, but not
|
||||
// remote). If the value is non-nil, it's remote (+ maybe also
|
||||
// local).
|
||||
clientsMesh map[key.Public]PacketForwarder
|
||||
clientsMesh map[key.NodePublic]PacketForwarder
|
||||
// sentTo tracks which peers have sent to which other peers,
|
||||
// and at which connection number. This isn't on sclient
|
||||
// because it includes intra-region forwarded packets as the
|
||||
// src.
|
||||
sentTo map[key.Public]map[key.Public]int64 // src => dst => dst's latest sclient.connNum
|
||||
sentTo map[key.NodePublic]map[key.NodePublic]int64 // src => dst => dst's latest sclient.connNum
|
||||
|
||||
// maps from netaddr.IPPort to a client's public key
|
||||
keyOfAddr map[netaddr.IPPort]key.Public
|
||||
keyOfAddr map[netaddr.IPPort]key.NodePublic
|
||||
}
|
||||
|
||||
// clientSet represents 1 or more *sclients.
|
||||
@@ -277,7 +276,7 @@ func (s *dupClientSet) removeClient(c *sclient) bool {
|
||||
// is a multiForwarder, which this package creates as needed if a
|
||||
// public key gets more than one PacketForwarder registered for it.
|
||||
type PacketForwarder interface {
|
||||
ForwardPacket(src, dst key.Public, payload []byte) error
|
||||
ForwardPacket(src, dst key.NodePublic, payload []byte) error
|
||||
}
|
||||
|
||||
// Conn is the subset of the underlying net.Conn the DERP Server needs.
|
||||
@@ -294,7 +293,7 @@ type Conn interface {
|
||||
|
||||
// NewServer returns a new DERP server. It doesn't listen on its own.
|
||||
// Connections are given to it via Server.Accept.
|
||||
func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
|
||||
@@ -306,14 +305,14 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
|
||||
packetsDroppedReason: metrics.LabelMap{Label: "reason"},
|
||||
packetsDroppedType: metrics.LabelMap{Label: "type"},
|
||||
clients: map[key.Public]clientSet{},
|
||||
clientsMesh: map[key.Public]PacketForwarder{},
|
||||
clients: map[key.NodePublic]clientSet{},
|
||||
clientsMesh: map[key.NodePublic]PacketForwarder{},
|
||||
netConns: map[Conn]chan struct{}{},
|
||||
memSys0: ms.Sys,
|
||||
watchers: map[*sclient]bool{},
|
||||
sentTo: map[key.Public]map[key.Public]int64{},
|
||||
sentTo: map[key.NodePublic]map[key.NodePublic]int64{},
|
||||
avgQueueDuration: new(uint64),
|
||||
keyOfAddr: map[netaddr.IPPort]key.Public{},
|
||||
keyOfAddr: map[netaddr.IPPort]key.NodePublic{},
|
||||
}
|
||||
s.initMetacert()
|
||||
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
|
||||
@@ -353,10 +352,10 @@ func (s *Server) HasMeshKey() bool { return s.meshKey != "" }
|
||||
func (s *Server) MeshKey() string { return s.meshKey }
|
||||
|
||||
// PrivateKey returns the server's private key.
|
||||
func (s *Server) PrivateKey() key.Private { return s.privateKey }
|
||||
func (s *Server) PrivateKey() key.NodePrivate { return s.privateKey }
|
||||
|
||||
// PublicKey returns the server's public key.
|
||||
func (s *Server) PublicKey() key.Public { return s.publicKey }
|
||||
func (s *Server) PublicKey() key.NodePublic { return s.publicKey }
|
||||
|
||||
// Close closes the server and waits for the connections to disconnect.
|
||||
func (s *Server) Close() error {
|
||||
@@ -447,7 +446,7 @@ func (s *Server) initMetacert() {
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(ProtocolVersion),
|
||||
Subject: pkix.Name{
|
||||
CommonName: fmt.Sprintf("derpkey%x", s.publicKey[:]),
|
||||
CommonName: fmt.Sprintf("derpkey%s", s.publicKey.UntypedHexString()),
|
||||
},
|
||||
// Windows requires NotAfter and NotBefore set:
|
||||
NotAfter: time.Now().Add(30 * 24 * time.Hour),
|
||||
@@ -515,7 +514,7 @@ func (s *Server) registerClient(c *sclient) {
|
||||
// presence changed.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *Server) broadcastPeerStateChangeLocked(peer key.Public, present bool) {
|
||||
func (s *Server) broadcastPeerStateChangeLocked(peer key.NodePublic, present bool) {
|
||||
for w := range s.watchers {
|
||||
w.peerStateChange = append(w.peerStateChange, peerConnState{peer: peer, present: present})
|
||||
go w.requestMeshUpdate()
|
||||
@@ -577,7 +576,7 @@ func (s *Server) unregisterClient(c *sclient) {
|
||||
// key has sent to previously (whether those sends were from a local
|
||||
// client or forwarded). It must only be called after the key has
|
||||
// been removed from clientsMesh.
|
||||
func (s *Server) notePeerGoneFromRegionLocked(key key.Public) {
|
||||
func (s *Server) notePeerGoneFromRegionLocked(key key.NodePublic) {
|
||||
if _, ok := s.clientsMesh[key]; ok {
|
||||
panic("usage")
|
||||
}
|
||||
@@ -663,7 +662,7 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
connectedAt: time.Now(),
|
||||
sendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
peerGone: make(chan key.Public),
|
||||
peerGone: make(chan key.NodePublic),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
}
|
||||
|
||||
@@ -774,8 +773,8 @@ func (c *sclient) handleFrameClosePeer(ft frameType, fl uint32) error {
|
||||
if !c.canMesh {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
var targetKey key.Public
|
||||
if _, err := io.ReadFull(c.br, targetKey[:]); err != nil {
|
||||
var targetKey key.NodePublic
|
||||
if err := targetKey.ReadRawWithoutAllocating(c.br); err != nil {
|
||||
return err
|
||||
}
|
||||
s := c.s
|
||||
@@ -845,10 +844,10 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
|
||||
// notePeerSendLocked records that src sent to dst. We keep track of
|
||||
// that so when src disconnects, we can tell dst (if it's still
|
||||
// around) that src is gone (a peerGone frame).
|
||||
func (s *Server) notePeerSendLocked(src key.Public, dst *sclient) {
|
||||
func (s *Server) notePeerSendLocked(src key.NodePublic, dst *sclient) {
|
||||
m, ok := s.sentTo[src]
|
||||
if !ok {
|
||||
m = map[key.Public]int64{}
|
||||
m = map[key.NodePublic]int64{}
|
||||
s.sentTo[src] = m
|
||||
}
|
||||
m[dst.key] = dst.connNum
|
||||
@@ -919,7 +918,7 @@ const (
|
||||
dropReasonDupClient // the public key is connected 2+ times (active/active, fighting)
|
||||
)
|
||||
|
||||
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.Public, reason dropReason) {
|
||||
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, reason dropReason) {
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedReasonCounters[reason].Add(1)
|
||||
if disco.LooksLikeDiscoWrapper(packetBytes) {
|
||||
@@ -982,7 +981,7 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
// requestPeerGoneWrite sends a request to write a "peer gone" frame
|
||||
// that the provided peer has disconnected. It blocks until either the
|
||||
// write request is scheduled, or the client has closed.
|
||||
func (c *sclient) requestPeerGoneWrite(peer key.Public) {
|
||||
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic) {
|
||||
select {
|
||||
case c.peerGone <- peer:
|
||||
case <-c.done:
|
||||
@@ -999,7 +998,7 @@ func (c *sclient) requestMeshUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) verifyClient(clientKey key.Public, info *clientInfo) error {
|
||||
func (s *Server) verifyClient(clientKey key.NodePublic, info *clientInfo) error {
|
||||
if !s.verifyClients {
|
||||
return nil
|
||||
}
|
||||
@@ -1018,9 +1017,9 @@ func (s *Server) verifyClient(clientKey key.Public, info *clientInfo) error {
|
||||
}
|
||||
|
||||
func (s *Server) sendServerKey(lw *lazyBufioWriter) error {
|
||||
buf := make([]byte, 0, len(magic)+len(s.publicKey))
|
||||
buf := make([]byte, 0, len(magic)+key.NodePublicRawLen)
|
||||
buf = append(buf, magic...)
|
||||
buf = append(buf, s.publicKey[:]...)
|
||||
buf = s.publicKey.AppendTo(buf)
|
||||
err := writeFrame(lw.bw(), frameServerKey, buf)
|
||||
lw.Flush() // redundant (no-op) flush to release bufio.Writer
|
||||
return err
|
||||
@@ -1084,21 +1083,14 @@ type serverInfo struct {
|
||||
TokenBucketBytesBurst int `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.Public) error {
|
||||
var nonce [24]byte
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.NodePublic) error {
|
||||
msg, err := json.Marshal(serverInfo{Version: ProtocolVersion})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgbox := box.Seal(nil, msg, &nonce, clientKey.B32(), s.privateKey.B32())
|
||||
if err := writeFrameHeader(bw.bw(), frameServerInfo, nonceLen+uint32(len(msgbox))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bw.Write(nonce[:]); err != nil {
|
||||
msgbox := s.privateKey.SealTo(clientKey, msg)
|
||||
if err := writeFrameHeader(bw.bw(), frameServerInfo, uint32(len(msgbox))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bw.Write(msgbox); err != nil {
|
||||
@@ -1110,7 +1102,7 @@ func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.Public) error
|
||||
// recvClientKey reads the frameClientInfo frame from the client (its
|
||||
// proof of identity) upon its initial connection. It should be
|
||||
// considered especially untrusted at this point.
|
||||
func (s *Server) recvClientKey(br *bufio.Reader) (clientKey key.Public, info *clientInfo, err error) {
|
||||
func (s *Server) recvClientKey(br *bufio.Reader) (clientKey key.NodePublic, info *clientInfo, err error) {
|
||||
fl, err := readFrameTypeHeader(br, frameClientInfo)
|
||||
if err != nil {
|
||||
return zpub, nil, err
|
||||
@@ -1124,21 +1116,17 @@ func (s *Server) recvClientKey(br *bufio.Reader) (clientKey key.Public, info *cl
|
||||
if fl > 256<<10 {
|
||||
return zpub, nil, errors.New("long client info")
|
||||
}
|
||||
if _, err := io.ReadFull(br, clientKey[:]); err != nil {
|
||||
if err := clientKey.ReadRawWithoutAllocating(br); err != nil {
|
||||
return zpub, nil, err
|
||||
}
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(br, nonce[:]); err != nil {
|
||||
return zpub, nil, fmt.Errorf("nonce: %v", err)
|
||||
}
|
||||
msgLen := int(fl - minLen)
|
||||
msgLen := int(fl - keyLen)
|
||||
msgbox := make([]byte, msgLen)
|
||||
if _, err := io.ReadFull(br, msgbox); err != nil {
|
||||
return zpub, nil, fmt.Errorf("msgbox: %v", err)
|
||||
}
|
||||
msg, ok := box.Open(nil, msgbox, &nonce, (*[32]byte)(&clientKey), s.privateKey.B32())
|
||||
msg, ok := s.privateKey.OpenFrom(clientKey, msgbox)
|
||||
if !ok {
|
||||
return zpub, nil, fmt.Errorf("msgbox: cannot open len=%d with client key %x", msgLen, clientKey[:])
|
||||
return zpub, nil, fmt.Errorf("msgbox: cannot open len=%d with client key %s", msgLen, clientKey)
|
||||
}
|
||||
info = new(clientInfo)
|
||||
if err := json.Unmarshal(msg, info); err != nil {
|
||||
@@ -1147,11 +1135,11 @@ func (s *Server) recvClientKey(br *bufio.Reader) (clientKey key.Public, info *cl
|
||||
return clientKey, info, nil
|
||||
}
|
||||
|
||||
func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Public, contents []byte, err error) {
|
||||
func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.NodePublic, contents []byte, err error) {
|
||||
if frameLen < keyLen {
|
||||
return zpub, nil, errors.New("short send packet frame")
|
||||
}
|
||||
if err := readPublicKey(br, &dstKey); err != nil {
|
||||
if err := dstKey.ReadRawWithoutAllocating(br); err != nil {
|
||||
return zpub, nil, err
|
||||
}
|
||||
packetLen := frameLen - keyLen
|
||||
@@ -1172,17 +1160,17 @@ func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Publi
|
||||
return dstKey, contents, nil
|
||||
}
|
||||
|
||||
// zpub is the key.Public zero value.
|
||||
var zpub key.Public
|
||||
// zpub is the key.NodePublic zero value.
|
||||
var zpub key.NodePublic
|
||||
|
||||
func (s *Server) recvForwardPacket(br *bufio.Reader, frameLen uint32) (srcKey, dstKey key.Public, contents []byte, err error) {
|
||||
func (s *Server) recvForwardPacket(br *bufio.Reader, frameLen uint32) (srcKey, dstKey key.NodePublic, contents []byte, err error) {
|
||||
if frameLen < keyLen*2 {
|
||||
return zpub, zpub, nil, errors.New("short send packet frame")
|
||||
}
|
||||
if _, err := io.ReadFull(br, srcKey[:]); err != nil {
|
||||
if err := srcKey.ReadRawWithoutAllocating(br); err != nil {
|
||||
return zpub, zpub, nil, err
|
||||
}
|
||||
if _, err := io.ReadFull(br, dstKey[:]); err != nil {
|
||||
if err := dstKey.ReadRawWithoutAllocating(br); err != nil {
|
||||
return zpub, zpub, nil, err
|
||||
}
|
||||
packetLen := frameLen - keyLen*2
|
||||
@@ -1206,19 +1194,19 @@ type sclient struct {
|
||||
connNum int64 // process-wide unique counter, incremented each Accept
|
||||
s *Server
|
||||
nc Conn
|
||||
key key.Public
|
||||
key key.NodePublic
|
||||
info clientInfo
|
||||
logf logger.Logf
|
||||
done <-chan struct{} // closed when connection closes
|
||||
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
discoSendQueue chan pkt // important packets queued to this client; never closed
|
||||
peerGone chan key.Public // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
isDup syncs.AtomicBool // whether more than 1 sclient for key is connected
|
||||
isDisabled syncs.AtomicBool // whether sends to this peer are disabled due to active/active dups
|
||||
done <-chan struct{} // closed when connection closes
|
||||
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
discoSendQueue chan pkt // important packets queued to this client; never closed
|
||||
peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
isDup syncs.AtomicBool // whether more than 1 sclient for key is connected
|
||||
isDisabled syncs.AtomicBool // whether sends to this peer are disabled due to active/active dups
|
||||
|
||||
// replaceLimiter controls how quickly two connections with
|
||||
// the same client key can kick each other off the server by
|
||||
@@ -1245,14 +1233,14 @@ type sclient struct {
|
||||
// peerConnState represents whether a peer is connected to the server
|
||||
// or not.
|
||||
type peerConnState struct {
|
||||
peer key.Public
|
||||
peer key.NodePublic
|
||||
present bool
|
||||
}
|
||||
|
||||
// pkt is a request to write a data frame to an sclient.
|
||||
type pkt struct {
|
||||
// src is the who's the sender of the packet.
|
||||
src key.Public
|
||||
src key.NodePublic
|
||||
|
||||
// enqueuedAt is when a packet was put onto a queue before it was sent,
|
||||
// and is used for reporting metrics on the duration of packets in the queue.
|
||||
@@ -1397,23 +1385,23 @@ func (c *sclient) sendKeepAlive() error {
|
||||
}
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.Public) error {
|
||||
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
|
||||
c.s.peerGoneFrames.Add(1)
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer[:])
|
||||
_, err := c.bw.Write(peer.AppendTo(nil))
|
||||
return err
|
||||
}
|
||||
|
||||
// sendPeerPresent sends a peerPresent frame, without flushing.
|
||||
func (c *sclient) sendPeerPresent(peer key.Public) error {
|
||||
func (c *sclient) sendPeerPresent(peer key.NodePublic) error {
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, keyLen); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer[:])
|
||||
_, err := c.bw.Write(peer.AppendTo(nil))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1465,7 +1453,7 @@ func (c *sclient) sendMeshUpdates() error {
|
||||
// DERPv2. The bytes of contents are only valid until this function
|
||||
// returns, do not retain slices.
|
||||
// It does not flush its bufio.Writer.
|
||||
func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
func (c *sclient) sendPacket(srcKey key.NodePublic, contents []byte) (err error) {
|
||||
defer func() {
|
||||
// Stats update.
|
||||
if err != nil {
|
||||
@@ -1481,14 +1469,13 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
withKey := !srcKey.IsZero()
|
||||
pktLen := len(contents)
|
||||
if withKey {
|
||||
pktLen += len(srcKey)
|
||||
pktLen += key.NodePublicRawLen
|
||||
}
|
||||
if err = writeFrameHeader(c.bw.bw(), frameRecvPacket, uint32(pktLen)); err != nil {
|
||||
return err
|
||||
}
|
||||
if withKey {
|
||||
err := writePublicKey(c.bw.bw(), &srcKey)
|
||||
if err != nil {
|
||||
if err := srcKey.WriteRawWithoutAllocating(c.bw.bw()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1498,7 +1485,7 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
|
||||
// AddPacketForwarder registers fwd as a packet forwarder for dst.
|
||||
// fwd must be comparable.
|
||||
func (s *Server) AddPacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
func (s *Server) AddPacketForwarder(dst key.NodePublic, fwd PacketForwarder) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if prev, ok := s.clientsMesh[dst]; ok {
|
||||
@@ -1530,7 +1517,7 @@ func (s *Server) AddPacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
|
||||
// RemovePacketForwarder removes fwd as a packet forwarder for dst.
|
||||
// fwd must be comparable.
|
||||
func (s *Server) RemovePacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
func (s *Server) RemovePacketForwarder(dst key.NodePublic, fwd PacketForwarder) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
v, ok := s.clientsMesh[dst]
|
||||
@@ -1592,7 +1579,7 @@ func (m multiForwarder) maxVal() (max uint8) {
|
||||
return
|
||||
}
|
||||
|
||||
func (m multiForwarder) ForwardPacket(src, dst key.Public, payload []byte) error {
|
||||
func (m multiForwarder) ForwardPacket(src, dst key.NodePublic, payload []byte) error {
|
||||
var fwd PacketForwarder
|
||||
var lowest uint8
|
||||
for k, v := range m {
|
||||
@@ -1692,37 +1679,6 @@ func (s *Server) ConsistencyCheck() error {
|
||||
return errors.New(strings.Join(errs, ", "))
|
||||
}
|
||||
|
||||
// readPublicKey reads key from br.
|
||||
// It is ~4x slower than io.ReadFull(br, key),
|
||||
// but it prevents key from escaping and thus being allocated.
|
||||
// If io.ReadFull(br, key) does not cause key to escape, use that instead.
|
||||
func readPublicKey(br *bufio.Reader, key *key.Public) error {
|
||||
// Do io.ReadFull(br, key), but one byte at a time, to avoid allocation.
|
||||
for i := range key {
|
||||
b, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key[i] = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writePublicKey writes key to bw.
|
||||
// It is ~3x slower than bw.Write(key[:]),
|
||||
// but it prevents key from escaping and thus being allocated.
|
||||
// If bw.Write(key[:]) does not cause key to escape, use that instead.
|
||||
func writePublicKey(bw *bufio.Writer, key *key.Public) error {
|
||||
// Do bw.Write(key[:]), but one byte at a time to avoid allocation.
|
||||
for _, b := range key {
|
||||
err := bw.WriteByte(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const minTimeBetweenLogs = 2 * time.Second
|
||||
|
||||
// BytesSentRecv records the number of bytes that have been sent since the last traffic check
|
||||
@@ -1731,7 +1687,7 @@ type BytesSentRecv struct {
|
||||
Sent uint64
|
||||
Recv uint64
|
||||
// Key is the public key of the client which sent/received these bytes.
|
||||
Key key.Public
|
||||
Key key.NodePublic
|
||||
}
|
||||
|
||||
// parseSSOutput parses the output from the specific call to ss in ServeDebugTraffic.
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -23,20 +22,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/net/nettest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newPrivateKey(tb testing.TB) (k key.Private) {
|
||||
tb.Helper()
|
||||
if _, err := crand.Read(k[:]); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestClientInfoUnmarshal(t *testing.T) {
|
||||
for i, in := range []string{
|
||||
`{"Version":5,"MeshKey":"abc"}`,
|
||||
@@ -54,15 +46,15 @@ func TestClientInfoUnmarshal(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
serverPrivateKey := key.NewNode()
|
||||
s := NewServer(serverPrivateKey, t.Logf)
|
||||
defer s.Close()
|
||||
|
||||
const numClients = 3
|
||||
var clientPrivateKeys []key.Private
|
||||
var clientKeys []key.Public
|
||||
var clientPrivateKeys []key.NodePrivate
|
||||
var clientKeys []key.NodePublic
|
||||
for i := 0; i < numClients; i++ {
|
||||
priv := newPrivateKey(t)
|
||||
priv := key.NewNode()
|
||||
clientPrivateKeys = append(clientPrivateKeys, priv)
|
||||
clientKeys = append(clientKeys, priv.Public())
|
||||
}
|
||||
@@ -225,7 +217,7 @@ func TestSendRecv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSendFreeze(t *testing.T) {
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
serverPrivateKey := key.NewNode()
|
||||
s := NewServer(serverPrivateKey, t.Logf)
|
||||
defer s.Close()
|
||||
s.WriteTimeout = 100 * time.Millisecond
|
||||
@@ -238,7 +230,7 @@ func TestSendFreeze(t *testing.T) {
|
||||
// Then cathy stops processing messsages.
|
||||
// That should not interfere with alice talking to bob.
|
||||
|
||||
newClient := func(name string, k key.Private) (c *Client, clientConn nettest.Conn) {
|
||||
newClient := func(name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
|
||||
t.Helper()
|
||||
c1, c2 := nettest.NewConn(name, 1024)
|
||||
go s.Accept(c1, bufio.NewReadWriter(bufio.NewReader(c1), bufio.NewWriter(c1)), name)
|
||||
@@ -252,13 +244,13 @@ func TestSendFreeze(t *testing.T) {
|
||||
return c, c2
|
||||
}
|
||||
|
||||
aliceKey := newPrivateKey(t)
|
||||
aliceKey := key.NewNode()
|
||||
aliceClient, aliceConn := newClient("alice", aliceKey)
|
||||
|
||||
bobKey := newPrivateKey(t)
|
||||
bobKey := key.NewNode()
|
||||
bobClient, bobConn := newClient("bob", bobKey)
|
||||
|
||||
cathyKey := newPrivateKey(t)
|
||||
cathyKey := key.NewNode()
|
||||
cathyClient, cathyConn := newClient("cathy", cathyKey)
|
||||
|
||||
var (
|
||||
@@ -427,7 +419,7 @@ type testServer struct {
|
||||
logf logger.Logf
|
||||
|
||||
mu sync.Mutex
|
||||
pubName map[key.Public]string
|
||||
pubName map[key.NodePublic]string
|
||||
clients map[*testClient]bool
|
||||
}
|
||||
|
||||
@@ -437,14 +429,14 @@ func (ts *testServer) addTestClient(c *testClient) {
|
||||
ts.clients[c] = true
|
||||
}
|
||||
|
||||
func (ts *testServer) addKeyName(k key.Public, name string) {
|
||||
func (ts *testServer) addKeyName(k key.NodePublic, name string) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
ts.pubName[k] = name
|
||||
ts.logf("test adding named key %q for %x", name, k)
|
||||
}
|
||||
|
||||
func (ts *testServer) keyName(k key.Public) string {
|
||||
func (ts *testServer) keyName(k key.NodePublic) string {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
if name, ok := ts.pubName[k]; ok {
|
||||
@@ -465,7 +457,7 @@ func (ts *testServer) close(t *testing.T) error {
|
||||
func newTestServer(t *testing.T) *testServer {
|
||||
t.Helper()
|
||||
logf := logger.WithPrefix(t.Logf, "derp-server: ")
|
||||
s := NewServer(newPrivateKey(t), logf)
|
||||
s := NewServer(key.NewNode(), logf)
|
||||
s.SetMeshKey("mesh-key")
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -491,7 +483,7 @@ func newTestServer(t *testing.T) *testServer {
|
||||
ln: ln,
|
||||
logf: logf,
|
||||
clients: map[*testClient]bool{},
|
||||
pubName: map[key.Public]string{},
|
||||
pubName: map[key.NodePublic]string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,20 +491,20 @@ type testClient struct {
|
||||
name string
|
||||
c *Client
|
||||
nc net.Conn
|
||||
pub key.Public
|
||||
pub key.NodePublic
|
||||
ts *testServer
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net.Conn, key.Private, logger.Logf) (*Client, error)) *testClient {
|
||||
func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net.Conn, key.NodePrivate, logger.Logf) (*Client, error)) *testClient {
|
||||
t.Helper()
|
||||
nc, err := net.Dial("tcp", ts.ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := newPrivateKey(t)
|
||||
ts.addKeyName(key.Public(), name)
|
||||
c, err := newClient(nc, key, logger.WithPrefix(t.Logf, "client-"+name+": "))
|
||||
k := key.NewNode()
|
||||
ts.addKeyName(k.Public(), name)
|
||||
c, err := newClient(nc, k, logger.WithPrefix(t.Logf, "client-"+name+": "))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -521,14 +513,14 @@ func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net
|
||||
nc: nc,
|
||||
c: c,
|
||||
ts: ts,
|
||||
pub: key.Public(),
|
||||
pub: k.Public(),
|
||||
}
|
||||
ts.addTestClient(tc)
|
||||
return tc
|
||||
}
|
||||
|
||||
func newRegularClient(t *testing.T, ts *testServer, name string) *testClient {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.Private, logf logger.Logf) (*Client, error) {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.NodePrivate, logf logger.Logf) (*Client, error) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
c, err := NewClient(priv, nc, brw, logf)
|
||||
if err != nil {
|
||||
@@ -541,7 +533,7 @@ func newRegularClient(t *testing.T, ts *testServer, name string) *testClient {
|
||||
}
|
||||
|
||||
func newTestWatcher(t *testing.T, ts *testServer, name string) *testClient {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.Private, logf logger.Logf) (*Client, error) {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.NodePrivate, logf logger.Logf) (*Client, error) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
c, err := NewClient(priv, nc, brw, logf, MeshKey("mesh-key"))
|
||||
if err != nil {
|
||||
@@ -555,9 +547,9 @@ func newTestWatcher(t *testing.T, ts *testServer, name string) *testClient {
|
||||
})
|
||||
}
|
||||
|
||||
func (tc *testClient) wantPresent(t *testing.T, peers ...key.Public) {
|
||||
func (tc *testClient) wantPresent(t *testing.T, peers ...key.NodePublic) {
|
||||
t.Helper()
|
||||
want := map[key.Public]bool{}
|
||||
want := map[key.NodePublic]bool{}
|
||||
for _, k := range peers {
|
||||
want[k] = true
|
||||
}
|
||||
@@ -569,7 +561,7 @@ func (tc *testClient) wantPresent(t *testing.T, peers ...key.Public) {
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PeerPresentMessage:
|
||||
got := key.Public(m)
|
||||
got := key.NodePublic(m)
|
||||
if !want[got] {
|
||||
t.Fatalf("got peer present for %v; want present for %v", tc.ts.keyName(got), logger.ArgWriter(func(bw *bufio.Writer) {
|
||||
for _, pub := range peers {
|
||||
@@ -587,7 +579,7 @@ func (tc *testClient) wantPresent(t *testing.T, peers ...key.Public) {
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *testClient) wantGone(t *testing.T, peer key.Public) {
|
||||
func (tc *testClient) wantGone(t *testing.T, peer key.NodePublic) {
|
||||
t.Helper()
|
||||
m, err := tc.c.recvTimeout(time.Second)
|
||||
if err != nil {
|
||||
@@ -595,7 +587,7 @@ func (tc *testClient) wantGone(t *testing.T, peer key.Public) {
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PeerGoneMessage:
|
||||
got := key.Public(m)
|
||||
got := key.NodePublic(m)
|
||||
if peer != got {
|
||||
t.Errorf("got gone message for %v; want gone for %v", tc.ts.keyName(got), tc.ts.keyName(peer))
|
||||
}
|
||||
@@ -654,21 +646,24 @@ func TestWatch(t *testing.T) {
|
||||
|
||||
type testFwd int
|
||||
|
||||
func (testFwd) ForwardPacket(key.Public, key.Public, []byte) error { panic("not called in tests") }
|
||||
func (testFwd) ForwardPacket(key.NodePublic, key.NodePublic, []byte) error {
|
||||
panic("not called in tests")
|
||||
}
|
||||
|
||||
func pubAll(b byte) (ret key.Public) {
|
||||
for i := range ret {
|
||||
ret[i] = b
|
||||
func pubAll(b byte) (ret key.NodePublic) {
|
||||
var bs [32]byte
|
||||
for i := range bs {
|
||||
bs[i] = b
|
||||
}
|
||||
return
|
||||
return key.NodePublicFromRaw32(mem.B(bs[:]))
|
||||
}
|
||||
|
||||
func TestForwarderRegistration(t *testing.T) {
|
||||
s := &Server{
|
||||
clients: make(map[key.Public]clientSet),
|
||||
clientsMesh: map[key.Public]PacketForwarder{},
|
||||
clients: make(map[key.NodePublic]clientSet),
|
||||
clientsMesh: map[key.NodePublic]PacketForwarder{},
|
||||
}
|
||||
want := func(want map[key.Public]PacketForwarder) {
|
||||
want := func(want map[key.NodePublic]PacketForwarder) {
|
||||
t.Helper()
|
||||
if got := s.clientsMesh; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("mismatch\n got: %v\nwant: %v\n", got, want)
|
||||
@@ -687,28 +682,28 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
|
||||
s.AddPacketForwarder(u1, testFwd(1))
|
||||
s.AddPacketForwarder(u2, testFwd(2))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
u2: testFwd(2),
|
||||
})
|
||||
|
||||
// Verify a remove of non-registered forwarder is no-op.
|
||||
s.RemovePacketForwarder(u2, testFwd(999))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
u2: testFwd(2),
|
||||
})
|
||||
|
||||
// Verify a remove of non-registered user is no-op.
|
||||
s.RemovePacketForwarder(u3, testFwd(1))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
u2: testFwd(2),
|
||||
})
|
||||
|
||||
// Actual removal.
|
||||
s.RemovePacketForwarder(u2, testFwd(2))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
})
|
||||
|
||||
@@ -716,7 +711,7 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
wantCounter(&s.multiForwarderCreated, 0)
|
||||
s.AddPacketForwarder(u1, testFwd(100))
|
||||
s.AddPacketForwarder(u1, testFwd(100)) // dup to trigger dup path
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
testFwd(100): 2,
|
||||
@@ -726,7 +721,7 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
|
||||
// Removing a forwarder in a multi set that doesn't exist; does nothing.
|
||||
s.RemovePacketForwarder(u1, testFwd(55))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
testFwd(100): 2,
|
||||
@@ -737,7 +732,7 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
// from being a multiForwarder.
|
||||
wantCounter(&s.multiForwarderDeleted, 0)
|
||||
s.RemovePacketForwarder(u1, testFwd(1))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(100),
|
||||
})
|
||||
wantCounter(&s.multiForwarderDeleted, 1)
|
||||
@@ -750,18 +745,18 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
}
|
||||
s.clients[u1] = singleClient{u1c}
|
||||
s.RemovePacketForwarder(u1, testFwd(100))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: nil,
|
||||
})
|
||||
|
||||
// But once that client disconnects, it should go away.
|
||||
s.unregisterClient(u1c)
|
||||
want(map[key.Public]PacketForwarder{})
|
||||
want(map[key.NodePublic]PacketForwarder{})
|
||||
|
||||
// But if it already has a forwarder, it's not removed.
|
||||
s.AddPacketForwarder(u1, testFwd(2))
|
||||
s.unregisterClient(u1c)
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(2),
|
||||
})
|
||||
|
||||
@@ -770,17 +765,17 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
// from nil to the new one, not a multiForwarder.
|
||||
s.clients[u1] = singleClient{u1c}
|
||||
s.clientsMesh[u1] = nil
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: nil,
|
||||
})
|
||||
s.AddPacketForwarder(u1, testFwd(3))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(3),
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetaCert(t *testing.T) {
|
||||
priv := newPrivateKey(t)
|
||||
priv := key.NewNode()
|
||||
pub := priv.Public()
|
||||
s := NewServer(priv, t.Logf)
|
||||
|
||||
@@ -792,7 +787,7 @@ func TestMetaCert(t *testing.T) {
|
||||
if fmt.Sprint(cert.SerialNumber) != fmt.Sprint(ProtocolVersion) {
|
||||
t.Errorf("serial = %v; want %v", cert.SerialNumber, ProtocolVersion)
|
||||
}
|
||||
if g, w := cert.Subject.CommonName, fmt.Sprintf("derpkey%x", pub[:]); g != w {
|
||||
if g, w := cert.Subject.CommonName, fmt.Sprintf("derpkey%s", pub.UntypedHexString()); g != w {
|
||||
t.Errorf("CommonName = %q; want %q", g, w)
|
||||
}
|
||||
}
|
||||
@@ -882,10 +877,10 @@ func TestClientSendPong(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerDupClients(t *testing.T) {
|
||||
serverPriv := newPrivateKey(t)
|
||||
serverPriv := key.NewNode()
|
||||
var s *Server
|
||||
|
||||
clientPriv := newPrivateKey(t)
|
||||
clientPriv := key.NewNode()
|
||||
clientPub := clientPriv.Public()
|
||||
|
||||
var c1, c2, c3 *sclient
|
||||
@@ -1141,12 +1136,12 @@ func BenchmarkSendRecv(b *testing.B) {
|
||||
}
|
||||
|
||||
func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
serverPrivateKey := newPrivateKey(b)
|
||||
serverPrivateKey := key.NewNode()
|
||||
s := NewServer(serverPrivateKey, logger.Discard)
|
||||
defer s.Close()
|
||||
|
||||
key := newPrivateKey(b)
|
||||
clientKey := key.Public()
|
||||
k := key.NewNode()
|
||||
clientKey := k.Public()
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -1170,7 +1165,7 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
go s.Accept(connIn, brwServer, "test-client")
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(connOut), bufio.NewWriter(connOut))
|
||||
client, err := NewClient(key, connOut, brw, logger.Discard)
|
||||
client, err := NewClient(k, connOut, brw, logger.Discard)
|
||||
if err != nil {
|
||||
b.Fatalf("client: %v", err)
|
||||
}
|
||||
@@ -1279,7 +1274,7 @@ func TestClientSendRateLimiting(t *testing.T) {
|
||||
c.setSendRateLimiter(ServerInfoMessage{})
|
||||
|
||||
pkt := make([]byte, 1000)
|
||||
if err := c.send(key.Public{}, pkt); err != nil {
|
||||
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writes1, bytes1 := cw.Stats()
|
||||
@@ -1290,7 +1285,7 @@ func TestClientSendRateLimiting(t *testing.T) {
|
||||
// Flood should all succeed.
|
||||
cw.ResetStats()
|
||||
for i := 0; i < 1000; i++ {
|
||||
if err := c.send(key.Public{}, pkt); err != nil {
|
||||
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1309,7 +1304,7 @@ func TestClientSendRateLimiting(t *testing.T) {
|
||||
TokenBucketBytesBurst: int(bytes1 * 2),
|
||||
})
|
||||
for i := 0; i < 1000; i++ {
|
||||
if err := c.send(key.Public{}, pkt); err != nil {
|
||||
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -50,7 +53,7 @@ type Client struct {
|
||||
MeshKey string // optional; for trusted clients
|
||||
IsProber bool // optional; for probers to optional declare themselves as such
|
||||
|
||||
privateKey key.Private
|
||||
privateKey key.NodePrivate
|
||||
logf logger.Logf
|
||||
dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
@@ -68,12 +71,12 @@ type Client struct {
|
||||
netConn io.Closer
|
||||
client *derp.Client
|
||||
connGen int // incremented once per new connection; valid values are >0
|
||||
serverPubKey key.Public
|
||||
serverPubKey key.NodePublic
|
||||
}
|
||||
|
||||
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
// To trigger a connection, use Connect.
|
||||
func NewRegionClient(privateKey key.Private, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
|
||||
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
@@ -93,7 +96,7 @@ func NewNetcheckClient(logf logger.Logf) *Client {
|
||||
|
||||
// NewClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
// To trigger a connection, use Connect.
|
||||
func NewClient(privateKey key.Private, serverURL string, logf logger.Logf) (*Client, error) {
|
||||
func NewClient(privateKey key.NodePrivate, serverURL string, logf logger.Logf) (*Client, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derphttp.NewClient: %v", err)
|
||||
@@ -124,14 +127,14 @@ func (c *Client) Connect(ctx context.Context) error {
|
||||
//
|
||||
// It only returns a non-zero value once a connection has succeeded
|
||||
// from an earlier call.
|
||||
func (c *Client) ServerPublicKey() key.Public {
|
||||
func (c *Client) ServerPublicKey() key.NodePublic {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.serverPubKey
|
||||
}
|
||||
|
||||
// SelfPublicKey returns our own public key.
|
||||
func (c *Client) SelfPublicKey() key.Public {
|
||||
func (c *Client) SelfPublicKey() key.NodePublic {
|
||||
return c.privateKey.Public()
|
||||
}
|
||||
|
||||
@@ -177,6 +180,20 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
|
||||
return fmt.Sprintf("https://%s/derp", node.HostName)
|
||||
}
|
||||
|
||||
// dialWebsocketFunc is non-nil (set by websocket.go's init) when compiled in.
|
||||
var dialWebsocketFunc func(ctx context.Context, urlStr string) (net.Conn, error)
|
||||
|
||||
func useWebsockets() bool {
|
||||
if runtime.GOOS == "js" {
|
||||
return true
|
||||
}
|
||||
if dialWebsocketFunc != nil {
|
||||
v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_DERP_WS_CLIENT"))
|
||||
return v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) connect(ctx context.Context, caller string) (client *derp.Client, connGen int, err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -229,10 +246,44 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}()
|
||||
|
||||
var node *tailcfg.DERPNode // nil when using c.url to dial
|
||||
if c.url != nil {
|
||||
switch {
|
||||
case useWebsockets():
|
||||
var urlStr string
|
||||
if c.url != nil {
|
||||
urlStr = c.url.String()
|
||||
} else {
|
||||
urlStr = c.urlString(reg.Nodes[0])
|
||||
}
|
||||
c.logf("%s: connecting websocket to %v", caller, urlStr)
|
||||
conn, err := dialWebsocketFunc(ctx, urlStr)
|
||||
if err != nil {
|
||||
c.logf("%s: websocket to %v error: %v", caller, urlStr, err)
|
||||
return nil, 0, err
|
||||
}
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
||||
derpClient, err := derp.NewClient(c.privateKey, conn, brw, c.logf,
|
||||
derp.MeshKey(c.MeshKey),
|
||||
derp.CanAckPings(c.canAckPings),
|
||||
derp.IsProber(c.IsProber),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if c.preferred {
|
||||
if err := derpClient.NotePreferred(true); err != nil {
|
||||
go conn.Close()
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
c.serverPubKey = derpClient.ServerPublicKey()
|
||||
c.client = derpClient
|
||||
c.netConn = tcpConn
|
||||
c.connGen++
|
||||
return c.client, c.connGen, nil
|
||||
case c.url != nil:
|
||||
c.logf("%s: connecting to %v", caller, c.url)
|
||||
tcpConn, err = c.dialURL(ctx)
|
||||
} else {
|
||||
default:
|
||||
c.logf("%s: connecting to derp-%d (%v)", caller, reg.RegionID, reg.RegionCode)
|
||||
tcpConn, node, err = c.dialRegion(ctx, reg)
|
||||
}
|
||||
@@ -264,8 +315,8 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}
|
||||
}()
|
||||
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
var serverPub key.Public // or zero if unknown (if not using TLS or TLS middlebox eats it)
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
var serverPub key.NodePublic // or zero if unknown (if not using TLS or TLS middlebox eats it)
|
||||
var serverProtoVersion int
|
||||
if c.useHTTPS() {
|
||||
tlsConn := c.tlsClient(tcpConn, node)
|
||||
@@ -378,7 +429,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
|
||||
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
|
||||
}
|
||||
hostOrIP := host
|
||||
dialer := netns.NewDialer()
|
||||
dialer := netns.NewDialer(c.logf)
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
|
||||
@@ -468,7 +519,7 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
|
||||
}
|
||||
|
||||
func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) {
|
||||
return netns.NewDialer().DialContext(ctx, proto, addr)
|
||||
return netns.NewDialer(c.logf).DialContext(ctx, proto, addr)
|
||||
}
|
||||
|
||||
// shouldDialProto reports whether an explicitly provided IPv4 or IPv6
|
||||
@@ -636,7 +687,7 @@ func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, pr
|
||||
return proxyConn, nil
|
||||
}
|
||||
|
||||
func (c *Client) Send(dstKey key.Public, b []byte) error {
|
||||
func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.Send")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -647,7 +698,7 @@ func (c *Client) Send(dstKey key.Public, b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) ForwardPacket(from, to key.Public, b []byte) error {
|
||||
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -728,7 +779,7 @@ func (c *Client) WatchConnectionChanges() error {
|
||||
// ClosePeer asks the server to close target's TCP connection.
|
||||
//
|
||||
// Only trusted connections (using MeshKey) are allowed to use this.
|
||||
func (c *Client) ClosePeer(target key.Public) error {
|
||||
func (c *Client) ClosePeer(target key.NodePublic) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ClosePeer")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -812,15 +863,15 @@ func (c *Client) closeForReconnect(brokenClient *derp.Client) {
|
||||
|
||||
var ErrClientClosed = errors.New("derphttp.Client closed")
|
||||
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.Public, serverProtoVersion int) {
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.NodePublic, serverProtoVersion int) {
|
||||
for _, cert := range certs {
|
||||
if cn := cert.Subject.CommonName; strings.HasPrefix(cn, "derpkey") {
|
||||
var err error
|
||||
serverPub, err = key.NewPublicFromHexMem(mem.S(strings.TrimPrefix(cn, "derpkey")))
|
||||
serverPub, err = key.ParseNodePublicUntyped(mem.S(strings.TrimPrefix(cn, "derpkey")))
|
||||
if err == nil && cert.SerialNumber.BitLen() <= 8 { // supports up to version 255
|
||||
return serverPub, int(cert.SerialNumber.Int64())
|
||||
}
|
||||
}
|
||||
}
|
||||
return key.Public{}, 0
|
||||
return key.NodePublic{}, 0
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/derp"
|
||||
)
|
||||
@@ -20,10 +21,15 @@ const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p := r.Header.Get("Upgrade"); p != "WebSocket" && p != "DERP" {
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
if up != "websocket" && up != "derp" {
|
||||
if up != "" {
|
||||
log.Printf("Weird upgrade: %q", up)
|
||||
}
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
|
||||
fastStart := r.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
@@ -45,9 +51,9 @@ func Handler(s *derp.Server) http.Handler {
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %x\r\n\r\n",
|
||||
"Derp-Public-Key: %s\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKey[:])
|
||||
pubKey.UntypedHexString())
|
||||
}
|
||||
|
||||
s.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
|
||||
@@ -18,13 +18,13 @@ import (
|
||||
)
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := key.NewPrivate()
|
||||
serverPrivateKey := key.NewNode()
|
||||
|
||||
const numClients = 3
|
||||
var clientPrivateKeys []key.Private
|
||||
var clientKeys []key.Public
|
||||
var clientPrivateKeys []key.NodePrivate
|
||||
var clientKeys []key.NodePublic
|
||||
for i := 0; i < numClients; i++ {
|
||||
priv := key.NewPrivate()
|
||||
priv := key.NewNode()
|
||||
clientPrivateKeys = append(clientPrivateKeys, priv)
|
||||
clientKeys = append(clientKeys, priv.Public())
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
//
|
||||
// To force RunWatchConnectionLoop to return quickly, its ctx needs to
|
||||
// be closed, and c itself needs to be closed.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.Public, infoLogf logger.Logf, add, remove func(key.Public)) {
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add, remove func(key.NodePublic)) {
|
||||
if infoLogf == nil {
|
||||
infoLogf = logger.Discard
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
const statusInterval = 10 * time.Second
|
||||
var (
|
||||
mu sync.Mutex
|
||||
present = map[key.Public]bool{}
|
||||
present = map[key.NodePublic]bool{}
|
||||
loggedConnected = false
|
||||
)
|
||||
clear := func() {
|
||||
@@ -49,7 +49,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
for k := range present {
|
||||
remove(k)
|
||||
}
|
||||
present = map[key.Public]bool{}
|
||||
present = map[key.NodePublic]bool{}
|
||||
}
|
||||
lastConnGen := 0
|
||||
lastStatus := time.Now()
|
||||
@@ -69,7 +69,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
updatePeer := func(k key.Public, isPresent bool) {
|
||||
updatePeer := func(k key.NodePublic, isPresent bool) {
|
||||
if isPresent {
|
||||
add(k)
|
||||
} else {
|
||||
@@ -127,9 +127,9 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case derp.PeerPresentMessage:
|
||||
updatePeer(key.Public(m), true)
|
||||
updatePeer(key.NodePublic(m), true)
|
||||
case derp.PeerGoneMessage:
|
||||
updatePeer(key.Public(m), false)
|
||||
updatePeer(key.NodePublic(m), false)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
33
derp/derphttp/websocket.go
Normal file
33
derp/derphttp/websocket.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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.
|
||||
|
||||
//go:build linux || js
|
||||
// +build linux js
|
||||
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp/wsconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dialWebsocketFunc = dialWebsocket
|
||||
}
|
||||
|
||||
func dialWebsocket(ctx context.Context, urlStr string) (net.Conn, error) {
|
||||
c, res, err := websocket.Dial(ctx, urlStr, &websocket.DialOptions{
|
||||
Subprotocols: []string{"derp"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("websocket Dial: %v, %+v", err, res)
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("websocket: connected to %v", urlStr)
|
||||
return wsconn.New(c), nil
|
||||
}
|
||||
104
derp/wsconn/wsconn.go
Normal file
104
derp/wsconn/wsconn.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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 wsconn contains an adapter type that turns
|
||||
// a websocket connection into a net.Conn.
|
||||
package wsconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
// New returns a net.Conn wrapper around c,
|
||||
// using c to send and receive binary messages with
|
||||
// chunks of bytes with no defined framing, effectively
|
||||
// discarding all WebSocket-level message framing.
|
||||
func New(c *websocket.Conn) net.Conn {
|
||||
return &websocketConn{c: c}
|
||||
}
|
||||
|
||||
// websocketConn implements derp.Conn around a *websocket.Conn,
|
||||
// treating a websocket.Conn as a byte stream, ignoring the WebSocket
|
||||
// frame/message boundaries.
|
||||
type websocketConn struct {
|
||||
c *websocket.Conn
|
||||
|
||||
// rextra are extra bytes owned by the reader.
|
||||
rextra []byte
|
||||
|
||||
mu sync.Mutex
|
||||
rdeadline time.Time
|
||||
cancelRead context.CancelFunc
|
||||
}
|
||||
|
||||
func (wc *websocketConn) LocalAddr() net.Addr { return addr{} }
|
||||
func (wc *websocketConn) RemoteAddr() net.Addr { return addr{} }
|
||||
|
||||
type addr struct{}
|
||||
|
||||
func (addr) Network() string { return "websocket" }
|
||||
func (addr) String() string { return "websocket" }
|
||||
|
||||
func (wc *websocketConn) Read(p []byte) (n int, err error) {
|
||||
// Drain any leftover from previously.
|
||||
n = copy(p, wc.rextra)
|
||||
if n > 0 {
|
||||
wc.rextra = wc.rextra[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
|
||||
wc.mu.Lock()
|
||||
if dl := wc.rdeadline; !dl.IsZero() {
|
||||
ctx, cancel = context.WithDeadline(context.Background(), wc.rdeadline)
|
||||
} else {
|
||||
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(30*24*time.Hour))
|
||||
wc.rdeadline = time.Time{}
|
||||
}
|
||||
wc.cancelRead = cancel
|
||||
wc.mu.Unlock()
|
||||
defer cancel()
|
||||
|
||||
_, buf, err := wc.c.Read(ctx)
|
||||
n = copy(p, buf)
|
||||
wc.rextra = buf[n:]
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (wc *websocketConn) Write(p []byte) (n int, err error) {
|
||||
err = wc.c.Write(context.Background(), websocket.MessageBinary, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) Close() error { return wc.c.Close(websocket.StatusNormalClosure, "close") }
|
||||
|
||||
func (wc *websocketConn) SetDeadline(t time.Time) error {
|
||||
wc.SetReadDeadline(t)
|
||||
wc.SetWriteDeadline(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) SetReadDeadline(t time.Time) error {
|
||||
wc.mu.Lock()
|
||||
defer wc.mu.Unlock()
|
||||
if !t.IsZero() && (wc.rdeadline.IsZero() || t.Before(wc.rdeadline)) && wc.cancelRead != nil {
|
||||
wc.cancelRead()
|
||||
}
|
||||
wc.rdeadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
@@ -25,7 +25,9 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// Magic is the 6 byte header of all discovery messages.
|
||||
@@ -106,12 +108,28 @@ func appendMsgHeader(b []byte, t MessageType, ver uint8, dataLen int) (all, data
|
||||
}
|
||||
|
||||
type Ping struct {
|
||||
// TxID is a random client-generated per-ping transaction ID.
|
||||
TxID [12]byte
|
||||
|
||||
// NodeKey is allegedly the ping sender's wireguard public key.
|
||||
// Old clients (~1.16.0 and earlier) don't send this field.
|
||||
// It shouldn't be trusted by itself, but can be combined with
|
||||
// netmap data to reduce the discokey:nodekey relation from 1:N to
|
||||
// 1:1.
|
||||
NodeKey key.NodePublic
|
||||
}
|
||||
|
||||
func (m *Ping) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypePing, v0, 12)
|
||||
copy(d, m.TxID[:])
|
||||
dataLen := 12
|
||||
hasKey := !m.NodeKey.IsZero()
|
||||
if hasKey {
|
||||
dataLen += key.NodePublicRawLen
|
||||
}
|
||||
ret, d := appendMsgHeader(b, TypePing, v0, dataLen)
|
||||
n := copy(d, m.TxID[:])
|
||||
if hasKey {
|
||||
m.NodeKey.AppendTo(d[:n])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -120,7 +138,12 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
|
||||
return nil, errShort
|
||||
}
|
||||
m = new(Ping)
|
||||
copy(m.TxID[:], p)
|
||||
p = p[copy(m.TxID[:], p):]
|
||||
// Deliberately lax on longer-than-expected messages, for future
|
||||
// compatibility.
|
||||
if len(p) >= key.NodePublicRawLen {
|
||||
m.NodeKey = key.NodePublicFromRaw32(mem.B(p[:key.NodePublicRawLen]))
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestMarshalAndParse(t *testing.T) {
|
||||
@@ -26,6 +28,14 @@ func TestMarshalAndParse(t *testing.T) {
|
||||
},
|
||||
want: "01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c",
|
||||
},
|
||||
{
|
||||
name: "ping_with_nodekey_src",
|
||||
m: &Ping{
|
||||
TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
|
||||
NodeKey: key.NodePublicFromRaw32(mem.B([]byte{1: 1, 2: 2, 30: 30, 31: 31})),
|
||||
},
|
||||
want: "01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 00 01 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1e 1f",
|
||||
},
|
||||
{
|
||||
name: "pong",
|
||||
m: &Pong{
|
||||
|
||||
@@ -32,3 +32,7 @@ userspace-sidecar:
|
||||
proxy:
|
||||
@kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | sed -e "s;{{DEST_IP}};$(DEST_IP);g" | kubectl create -f-
|
||||
|
||||
subnet-router:
|
||||
@kubectl delete -f subnet.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | sed -e "s;{{ROUTES}};$(ROUTES);g" | kubectl create -f-
|
||||
|
||||
@@ -27,12 +27,12 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
|
||||
Configure RBAC to allow the Tailscale pod to read/write the `tailscale` secret.
|
||||
```bash
|
||||
export SA_NAME=tailscale
|
||||
export KUBE_SECRET=tailscale
|
||||
export KUBE_SECRET=tailscale-auth
|
||||
make rbac
|
||||
```
|
||||
|
||||
### Sample Sidecar
|
||||
Running as a sidecar allows you to directly expose a Kubernetes pod over Tailscale. This is particularly useful if you do not wish to expose a service on the public internet. This method allows bi-directional connectivty between the pod and other devices on the Tailnet. You can use [ACLs](https://tailscale.com/kb/1018/acls/) to control traffic flow.
|
||||
Running as a sidecar allows you to directly expose a Kubernetes pod over Tailscale. This is particularly useful if you do not wish to expose a service on the public internet. This method allows bi-directional connectivity between the pod and other devices on the Tailnet. You can use [ACLs](https://tailscale.com/kb/1018/acls/) to control traffic flow.
|
||||
|
||||
1. Create and login to the sample nginx pod with a Tailscale sidecar
|
||||
|
||||
@@ -107,4 +107,41 @@ Running a Tailscale proxy allows you to provide inbound connectivity to a Kubern
|
||||
|
||||
```bash
|
||||
curl "http://$(tailscale ip -4 proxy)"
|
||||
```
|
||||
```
|
||||
|
||||
### Subnet Router
|
||||
|
||||
Running a Tailscale [subnet router](https://tailscale.com/kb/1019/subnets/) allows you to access
|
||||
the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tailscale.
|
||||
|
||||
1. Identify the Pod/Service CIDRs that cover your Kubernetes cluster. These will vary depending on [which CNI](https://kubernetes.io/docs/concepts/cluster-administration/networking/) you are using and on the Cloud Provider you are using. Add these to the `ROUTES` variable as comma-separated values.
|
||||
|
||||
```bash
|
||||
SERVICE_CIDR=10.20.0.0/16
|
||||
POD_CIDR=10.42.0.0/15
|
||||
export ROUTES=$SERVICE_CIDR,$POD_CIDR
|
||||
```
|
||||
|
||||
1. Deploy the subnet-router pod.
|
||||
|
||||
```bash
|
||||
make subnet-router
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
kubectl logs subnet-router
|
||||
```
|
||||
|
||||
1. In the [Tailscale admin console](https://login.tailscale.com/admin/machines), ensure that the
|
||||
routes for the subnet-router are enabled.
|
||||
|
||||
1. Make sure that any client you want to connect from has `--accept-routes` enabled.
|
||||
|
||||
1. Check if you can connect to a `ClusterIP` or a `PodIP` over Tailscale:
|
||||
|
||||
```bash
|
||||
# Get the Service IP
|
||||
INTERNAL_IP="$(kubectl get svc <SVC_NAME> -o=jsonpath='{.spec.clusterIP}')"
|
||||
# or, the Pod IP
|
||||
# INTERNAL_IP="$(kubectl get po <POD_NAME> -o=jsonpath='{.status.podIP}')"
|
||||
INTERNAL_PORT=8080
|
||||
curl http://$INTERNAL_IP:$INTERNAL_PORT
|
||||
```
|
||||
|
||||
@@ -53,7 +53,7 @@ tailscale --socket=/tmp/tailscaled.sock up ${UP_ARGS}
|
||||
|
||||
if [[ ! -z "${DEST_IP}" ]]; then
|
||||
echo "Adding iptables rule for DNAT"
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale ip -4)" -j DNAT --to-destination "${DEST_IP}"
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${DEST_IP}"
|
||||
fi
|
||||
|
||||
wait ${PID}
|
||||
wait ${PID}
|
||||
|
||||
32
docs/k8s/subnet.yaml
Normal file
32
docs/k8s/subnet.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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.
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: subnet-router
|
||||
labels:
|
||||
app: tailscale
|
||||
spec:
|
||||
serviceAccountName: "{{SA_NAME}}"
|
||||
containers:
|
||||
- name: tailscale
|
||||
imagePullPolicy: Always
|
||||
image: "{{IMAGE_TAG}}"
|
||||
env:
|
||||
# Store the state in a k8s secret
|
||||
- name: KUBE_SECRET
|
||||
value: "{{KUBE_SECRET}}"
|
||||
- name: USERSPACE
|
||||
value: "true"
|
||||
- name: AUTH_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: AUTH_KEY
|
||||
optional: true
|
||||
- name: ROUTES
|
||||
value: "{{ROUTES}}"
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
27
go.mod
27
go.mod
@@ -4,6 +4,7 @@ go 1.17
|
||||
|
||||
require (
|
||||
filippo.io/mkcert v1.4.3
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aws/aws-sdk-go v1.38.52
|
||||
@@ -11,11 +12,10 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/config v1.8.3
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.12.0
|
||||
github.com/coreos/go-iptables v0.6.0
|
||||
github.com/creack/pty v1.1.16
|
||||
github.com/creack/pty v1.1.17
|
||||
github.com/dave/jennifer v1.4.1
|
||||
github.com/frankban/quicktest v1.13.1
|
||||
github.com/frankban/quicktest v1.14.0
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1
|
||||
github.com/godbus/dbus/v5 v5.0.5
|
||||
github.com/google/go-cmp v0.5.6
|
||||
@@ -39,24 +39,27 @@ require (
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
golang.org/x/net v0.0.0-20210927181540-4e4d966f7476
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6
|
||||
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
|
||||
golang.org/x/tools v0.1.7
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211012062646-82d2aa87aa62
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10
|
||||
honnef.co/go/tools v0.2.1
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
inet.af/netstack v0.0.0-20211101182044-1c1bcf452982
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
nhooyr.io/websocket v1.8.7
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -190,12 +193,14 @@ require (
|
||||
github.com/ultraware/funlen v0.0.3 // indirect
|
||||
github.com/ultraware/whitespace v0.0.4 // indirect
|
||||
github.com/uudashr/gocognit v1.0.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
105
go.sum
105
go.sum
@@ -39,6 +39,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
@@ -103,8 +105,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.16 h1:vfetlOf3A+9YKggibynnX9mnFjuSVvkRj+IWpcTSLEQ=
|
||||
github.com/creack/pty v1.1.16/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/daixiang0/gci v0.2.4/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD/hfR4=
|
||||
github.com/daixiang0/gci v0.2.7 h1:bosLNficubzJZICsVzxuyNc6oAbdz0zcqLG2G/RxtY4=
|
||||
github.com/daixiang0/gci v0.2.7/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
|
||||
@@ -127,12 +129,17 @@ github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/frankban/quicktest v1.13.1 h1:xVm/f9seEhZFL9+n5kv5XLrGwy6elc4V9v/XFY2vmd8=
|
||||
github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
||||
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
|
||||
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
@@ -152,11 +159,18 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
|
||||
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1 h1:4dntyT+x6QTOSCIrgczbQ+ockAEha0cfxD5Wi0iCzjY=
|
||||
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||
@@ -181,6 +195,14 @@ github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/godbus/dbus/v5 v5.0.5 h1:9Eg0XUhQxtkV8ykTMKtMMYY72g4NgxtRq4jgh4Ih5YM=
|
||||
github.com/godbus/dbus/v5 v5.0.5/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY=
|
||||
@@ -199,11 +221,14 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||
@@ -251,6 +276,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
|
||||
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@@ -274,6 +300,8 @@ github.com/goreleaser/fileglob v0.3.1 h1:OTFDWqUUHjQazk2N5GdUqEbqT/grBnRARaAXsV0
|
||||
github.com/goreleaser/fileglob v0.3.1/go.mod h1:kNcPrPzjCp+Ox3jmXLU5QEsjhqrtLBm6OnXAif8KRl8=
|
||||
github.com/goreleaser/nfpm v1.10.3 h1:NzpWKKzSFr7JOn55XN0SskyFOjP6BkvRt3JujoX8fws=
|
||||
github.com/goreleaser/nfpm v1.10.3/go.mod h1:EEC7YD5wi+ol0MiAshpgPANBOkjXDl7wqTLVk68OBsk=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
@@ -350,6 +378,8 @@ github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190 h1:iycCSDo8EKVueI9sfVBBJmtNn9DnXV/K1YWwEJO+uOs=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
@@ -362,6 +392,7 @@ github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
|
||||
@@ -383,6 +414,8 @@ github.com/kunwardeep/paralleltest v1.0.2 h1:/jJRv0TiqPoEy/Y8dQxCFJhD56uS/pnvtat
|
||||
github.com/kunwardeep/paralleltest v1.0.2/go.mod h1:ZPqNm1fVHPllh5LPVujzbVz1JN2GhLxSfY+oqUsvG30=
|
||||
github.com/kyoh86/exportloopref v0.1.8 h1:5Ry/at+eFdkX9Vsdw3qU4YkvGtzuVfzT4X7S77LoN/M=
|
||||
github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
@@ -453,7 +486,13 @@ github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
|
||||
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moricho/tparallel v0.2.1 h1:95FytivzT6rYzdJLdtfn6m1bfFJylOJK41+lgv/EHf4=
|
||||
github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k=
|
||||
@@ -611,6 +650,8 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI=
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
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/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
@@ -632,6 +673,11 @@ github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9r
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA=
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
|
||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
@@ -645,6 +691,10 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
|
||||
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
||||
github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6 h1:167a2omrzz+nN9Of6lN/0yOB9itzw+IOioRThNZ30jA=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
@@ -663,13 +713,15 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -687,8 +739,9 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -756,8 +809,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210927181540-4e4d966f7476 h1:s5hu7bTnLKswvidgtqc4GwsW83m9LZu8UAqzmWOZtI4=
|
||||
golang.org/x/net v0.0.0-20210927181540-4e4d966f7476/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211101193420-4a448f8816b3/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495 h1:cjxxlQm6d4kYbhpZ2ghvmI8xnq0AG+jXmzrhzfkyu5A=
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -804,11 +858,13 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -834,8 +890,9 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 h1:WecRHqgE09JBkh/584XIE6PMz5KKE/vER4izNUi30AQ=
|
||||
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
@@ -850,6 +907,7 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs=
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -914,9 +972,15 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211012062646-82d2aa87aa62 h1:c39XZipaMOiSSqTCpqJmYgnzscTBGLFPgMmGvubmZ6E=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211012062646-82d2aa87aa62/go.mod h1:id8Oh3eCCmpj9uVGWVjsUAl6UPX5ysMLzu6QxJU2UOU=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211115224047-111e0566dce3 h1:7BFThRTwBwTLoMomQ/Y0GqY1VLH9D7kbbTNsfxl2fU0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211115224047-111e0566dce3/go.mod h1:evxZIqfCetExY5piKXGAxJYwvXWkps9zTCkWpkoGFxw=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211116194326-3cae233d69f7 h1:ZeHUKruJlkbSvafSH7GrDzMDXf7+/0T5sEKE8A9rEiE=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211116194326-3cae233d69f7/go.mod h1:evxZIqfCetExY5piKXGAxJYwvXWkps9zTCkWpkoGFxw=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45 h1:mEVhdMPTuebD9IUXOUB5Q2sjZpcmzkahHWd6DrGpLHA=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45/go.mod h1:evxZIqfCetExY5piKXGAxJYwvXWkps9zTCkWpkoGFxw=
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10 h1:HmjzJnb+G4NCdX+sfjsQlsxGPuYaThxRbZUZFLyR0/s=
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10/go.mod h1:v7w/8FC48tTBm1IzScDVPEEb0/GjLta+T0ybpP9UWRg=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -950,6 +1014,7 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -989,10 +1054,10 @@ honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1 h1:mxmfTV6kjXTlFqqFETnG9FQZzNFc6AKunZVAgQ3b7WA=
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e h1:z11NK94NQcI3DA+a3pUC/2dRYTph1kPX6B0FnCaMDzk=
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e/go.mod h1:fG3G1dekmK8oDX3iVzt8c0zICLMLSN8SjdxbXVt0WjU=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=
|
||||
inet.af/netstack v0.0.0-20211101182044-1c1bcf452982 h1:hYciifHEv98/p8ln52ybKhgQpGouZWALFxxFE65RVdU=
|
||||
inet.af/netstack v0.0.0-20211101182044-1c1bcf452982/go.mod h1:fG3G1dekmK8oDX3iVzt8c0zICLMLSN8SjdxbXVt0WjU=
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763 h1:gPSJmmVzmdy4kHhlCMx912GdiUz3k/RzJGg0ADqy1dg=
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756 h1:muIT3C1rH3/xpvIH8blKkMvhctV7F+OtZqs7kcwHDBQ=
|
||||
@@ -1006,6 +1071,8 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphD
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 h1:iAEkCBPbRaflBgZ7o9gjVUuWuvWeV4sytFWg9o+Pj2k=
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237/go.mod h1:/xvNRWUqm0+/ZMiF4EX00vrSCMsE4/NHb+Pt3freEeQ=
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -267,7 +268,7 @@ func selfCheckLocked() {
|
||||
// OverallError returns a summary of the health state.
|
||||
//
|
||||
// If there are multiple problems, the error will be of type
|
||||
// multierror.MultipleErrors.
|
||||
// multierr.Error.
|
||||
func OverallError() error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -336,7 +337,7 @@ func overallErrorLocked() error {
|
||||
// Not super efficient (stringifying these in a sort), but probably max 2 or 3 items.
|
||||
return errs[i].Error() < errs[j].Error()
|
||||
})
|
||||
return multierror.New(errs)
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -347,6 +348,12 @@ var (
|
||||
receiveFuncs = []*ReceiveFuncStats{&ReceiveIPv4, &ReceiveIPv6, &ReceiveDERP}
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "js" {
|
||||
receiveFuncs = receiveFuncs[2:] // ignore IPv4 and IPv6
|
||||
}
|
||||
}
|
||||
|
||||
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
|
||||
type ReceiveFuncStats struct {
|
||||
// name is the name of the receive func.
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -28,7 +31,7 @@ func New() *tailcfg.Hostinfo {
|
||||
IPNVersion: version.Long,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: getOSVersion(),
|
||||
OSVersion: GetOSVersion(),
|
||||
Package: packageType(),
|
||||
GoArch: runtime.GOARCH,
|
||||
DeviceModel: deviceModel(),
|
||||
@@ -37,7 +40,8 @@ func New() *tailcfg.Hostinfo {
|
||||
|
||||
var osVersion func() string // non-nil on some platforms
|
||||
|
||||
func getOSVersion() string {
|
||||
// GetOSVersion returns the OSVersion of current host if available.
|
||||
func GetOSVersion() string {
|
||||
if s, _ := osVersionAtomic.Load().(string); s != "" {
|
||||
return s
|
||||
}
|
||||
@@ -82,6 +86,7 @@ const (
|
||||
AzureAppService = EnvType("az")
|
||||
AWSFargate = EnvType("fg")
|
||||
FlyDotIo = EnvType("fly")
|
||||
Kubernetes = EnvType("k8s")
|
||||
)
|
||||
|
||||
var envType atomic.Value // of EnvType
|
||||
@@ -136,6 +141,9 @@ func getEnvType() EnvType {
|
||||
if inFlyDotIo() {
|
||||
return FlyDotIo
|
||||
}
|
||||
if inKubernetes() {
|
||||
return Kubernetes
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -212,3 +220,62 @@ func inFlyDotIo() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inKubernetes() bool {
|
||||
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type etcAptSrcResult struct {
|
||||
mod time.Time
|
||||
disabled bool
|
||||
}
|
||||
|
||||
var etcAptSrcCache atomic.Value // of etcAptSrcResult
|
||||
|
||||
// DisabledEtcAptSource reports whether Ubuntu (or similar) has disabled
|
||||
// the /etc/apt/sources.list.d/tailscale.list file contents upon upgrade
|
||||
// to a new release of the distro.
|
||||
//
|
||||
// See https://github.com/tailscale/tailscale/issues/3177
|
||||
func DisabledEtcAptSource() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
const path = "/etc/apt/sources.list.d/tailscale.list"
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil || !fi.Mode().IsRegular() {
|
||||
return false
|
||||
}
|
||||
mod := fi.ModTime()
|
||||
if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod == mod {
|
||||
return c.disabled
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
v := etcAptSourceFileIsDisabled(f)
|
||||
etcAptSrcCache.Store(etcAptSrcResult{mod: mod, disabled: v})
|
||||
return v
|
||||
}
|
||||
|
||||
func etcAptSourceFileIsDisabled(r io.Reader) bool {
|
||||
bs := bufio.NewScanner(r)
|
||||
disabled := false // did we find the "disabled on upgrade" comment?
|
||||
for bs.Scan() {
|
||||
line := strings.TrimSpace(bs.Text())
|
||||
if strings.Contains(line, "# disabled on upgrade") {
|
||||
disabled = true
|
||||
}
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
// Well, it has some contents in it at least.
|
||||
return false
|
||||
}
|
||||
return disabled
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package hostinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -27,3 +28,25 @@ func TestOSVersion(t *testing.T) {
|
||||
}
|
||||
t.Logf("Got: %#q", osVersion())
|
||||
}
|
||||
|
||||
func TestEtcAptSourceFileIsDisabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"empty", "", false},
|
||||
{"normal", "deb foo\n", false},
|
||||
{"normal-commented", "# deb foo\n", false},
|
||||
{"normal-disabled-by-ubuntu", "# deb foo # disabled on upgrade to dingus\n", true},
|
||||
{"normal-disabled-then-uncommented", "deb foo # disabled on upgrade to dingus\n", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := etcAptSourceFileIsDisabled(strings.NewReader(tt.in))
|
||||
if got != tt.want {
|
||||
t.Errorf("got %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/structs"
|
||||
)
|
||||
@@ -48,7 +49,7 @@ type EngineStatus struct {
|
||||
RBytes, WBytes int64
|
||||
NumLive int
|
||||
LiveDERPs int // number of active DERP connections
|
||||
LivePeers map[tailcfg.NodeKey]ipnstate.PeerStatusLite
|
||||
LivePeers map[key.NodePublic]ipnstate.PeerStatusLite
|
||||
}
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
@@ -47,9 +46,9 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/deephash"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
@@ -97,6 +96,7 @@ type LocalBackend struct {
|
||||
gotPortPollRes chan struct{} // closed upon first readPoller result
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
varRoot string // or empty if SetVarRoot never called
|
||||
|
||||
filterHash deephash.Sum
|
||||
|
||||
@@ -122,6 +122,7 @@ type LocalBackend struct {
|
||||
engineStatus ipn.EngineStatus
|
||||
endpoints []tailcfg.Endpoint
|
||||
blocked bool
|
||||
keyExpired bool
|
||||
authURL string // cleared on Notify
|
||||
authURLSticky string // not cleared on Notify
|
||||
interact bool
|
||||
@@ -294,8 +295,8 @@ func (b *LocalBackend) Prefs() *ipn.Prefs {
|
||||
p := b.prefs.Clone()
|
||||
if p != nil && p.Persist != nil {
|
||||
p.Persist.LegacyFrontendPrivateMachineKey = key.MachinePrivate{}
|
||||
p.Persist.PrivateNodeKey = wgkey.Private{}
|
||||
p.Persist.OldPrivateNodeKey = wgkey.Private{}
|
||||
p.Persist.PrivateNodeKey = key.NodePrivate{}
|
||||
p.Persist.OldPrivateNodeKey = key.NodePrivate{}
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -335,8 +336,8 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
|
||||
if err := health.OverallError(); err != nil {
|
||||
switch e := err.(type) {
|
||||
case multierror.MultipleErrors:
|
||||
for _, err := range e {
|
||||
case multierr.Error:
|
||||
for _, err := range e.Errors() {
|
||||
s.Health = append(s.Health, err.Error())
|
||||
}
|
||||
default:
|
||||
@@ -389,7 +390,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP())
|
||||
}
|
||||
}
|
||||
sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
|
||||
sb.AddPeer(p.Key, &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
@@ -448,20 +449,39 @@ func (b *LocalBackend) SetDecompressor(fn func() (controlclient.Decompressor, er
|
||||
// Among other things, this is where we update the netmap, packet filters, DNS and DERP maps.
|
||||
func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// The following do not depend on any data for which we need to lock b.
|
||||
if st.Err != "" {
|
||||
if st.Err != nil {
|
||||
// TODO(crawshaw): display in the UI.
|
||||
if st.Err == "EOF" {
|
||||
if errors.Is(st.Err, io.EOF) {
|
||||
b.logf("[v1] Received error: EOF")
|
||||
} else {
|
||||
b.logf("Received error: %v", st.Err)
|
||||
return
|
||||
}
|
||||
b.logf("Received error: %v", st.Err)
|
||||
var uerr controlclient.UserVisibleError
|
||||
if errors.As(st.Err, &uerr) {
|
||||
s := uerr.UserVisibleError()
|
||||
b.send(ipn.Notify{ErrMessage: &s})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
wasBlocked := b.blocked
|
||||
keyExpiryExtended := false
|
||||
if st.NetMap != nil {
|
||||
wasExpired := b.keyExpired
|
||||
isExpired := !st.NetMap.Expiry.IsZero() && st.NetMap.Expiry.Before(time.Now())
|
||||
if wasExpired && !isExpired {
|
||||
keyExpiryExtended = true
|
||||
}
|
||||
b.keyExpired = isExpired
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
if keyExpiryExtended && wasBlocked {
|
||||
// Key extended, unblock the engine
|
||||
b.blockEngineUpdates(false)
|
||||
}
|
||||
|
||||
if st.LoginFinished != nil && wasBlocked {
|
||||
// Auth completed, unblock the engine
|
||||
b.blockEngineUpdates(false)
|
||||
@@ -845,7 +865,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
})
|
||||
}
|
||||
|
||||
var discoPublic tailcfg.DiscoKey
|
||||
var discoPublic key.DiscoPublic
|
||||
if controlclient.Debug.Disco {
|
||||
discoPublic = b.e.DiscoPublicKey()
|
||||
}
|
||||
@@ -1022,14 +1042,29 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{
|
||||
// Given that "internal" routes don't leave the device, we choose to
|
||||
// trust them more, allowing access to them when an Exit Node is enabled.
|
||||
func internalAndExternalInterfaces() (internal, external []netaddr.IPPrefix, err error) {
|
||||
if err := interfaces.ForeachInterfaceAddress(func(iface interfaces.Interface, pfx netaddr.IPPrefix) {
|
||||
il, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return internalAndExternalInterfacesFrom(il, runtime.GOOS)
|
||||
}
|
||||
|
||||
func internalAndExternalInterfacesFrom(il interfaces.List, goos string) (internal, external []netaddr.IPPrefix, err error) {
|
||||
// We use an IPSetBuilder here to canonicalize the prefixes
|
||||
// and to remove any duplicate entries.
|
||||
var internalBuilder, externalBuilder netaddr.IPSetBuilder
|
||||
if err := il.ForeachInterfaceAddress(func(iface interfaces.Interface, pfx netaddr.IPPrefix) {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP()) {
|
||||
return
|
||||
}
|
||||
if pfx.IsSingleIP() {
|
||||
return
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if iface.IsLoopback() {
|
||||
internalBuilder.AddPrefix(pfx)
|
||||
return
|
||||
}
|
||||
if goos == "windows" {
|
||||
// Windows Hyper-V prefixes all MAC addresses with 00:15:5d.
|
||||
// https://docs.microsoft.com/en-us/troubleshoot/windows-server/virtualization/default-limit-256-dynamic-mac-addresses
|
||||
//
|
||||
@@ -1040,16 +1075,24 @@ func internalAndExternalInterfaces() (internal, external []netaddr.IPPrefix, err
|
||||
// configuration breaks WSL2 DNS without this.
|
||||
mac := iface.Interface.HardwareAddr
|
||||
if len(mac) == 6 && mac[0] == 0x00 && mac[1] == 0x15 && mac[2] == 0x5d {
|
||||
internal = append(internal, pfx)
|
||||
internalBuilder.AddPrefix(pfx)
|
||||
return
|
||||
}
|
||||
}
|
||||
external = append(external, pfx)
|
||||
externalBuilder.AddPrefix(pfx)
|
||||
}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
iSet, err := internalBuilder.IPSet()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
eSet, err := externalBuilder.IPSet()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return internal, external, nil
|
||||
return iSet.Prefixes(), eSet.Prefixes(), nil
|
||||
}
|
||||
|
||||
func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
|
||||
@@ -1483,7 +1526,15 @@ func (b *LocalBackend) StartLoginInteractive() {
|
||||
if url != "" {
|
||||
b.popBrowserAuthNow()
|
||||
} else {
|
||||
cc.Login(nil, controlclient.LoginInteractive)
|
||||
flags := controlclient.LoginInteractive
|
||||
if runtime.GOOS == "js" {
|
||||
// The js/wasm client has no state storage so for now
|
||||
// treat all interactive logins as ephemeral.
|
||||
// TODO(bradfitz): if we start using browser LocalStorage
|
||||
// or something, then rethink this.
|
||||
flags |= controlclient.LoginEphemeral
|
||||
}
|
||||
cc.Login(nil, flags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1529,7 +1580,7 @@ func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret ipn.EngineSt
|
||||
var peerStats, peerKeys strings.Builder
|
||||
|
||||
ret.LiveDERPs = s.DERPs
|
||||
ret.LivePeers = map[tailcfg.NodeKey]ipnstate.PeerStatusLite{}
|
||||
ret.LivePeers = map[key.NodePublic]ipnstate.PeerStatusLite{}
|
||||
for _, p := range s.Peers {
|
||||
if !p.LastHandshake.IsZero() {
|
||||
fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes)
|
||||
@@ -1738,6 +1789,12 @@ func (b *LocalBackend) NetMap() *netmap.NetworkMap {
|
||||
return b.netMap
|
||||
}
|
||||
|
||||
func (b *LocalBackend) isEngineBlocked() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.blocked
|
||||
}
|
||||
|
||||
// blockEngineUpdate sets b.blocked to block, while holding b.mu. Its
|
||||
// indirect effect is to turn b.authReconfig() into a no-op if block
|
||||
// is true.
|
||||
@@ -1966,34 +2023,29 @@ func normalizeResolver(cfg dnstype.Resolver) dnstype.Resolver {
|
||||
return cfg
|
||||
}
|
||||
|
||||
// SetVarRoot sets the root directory of Tailscale's writable
|
||||
// storage area . (e.g. "/var/lib/tailscale")
|
||||
//
|
||||
// It should only be called before the LocalBackend is used.
|
||||
func (b *LocalBackend) SetVarRoot(dir string) {
|
||||
b.varRoot = dir
|
||||
}
|
||||
|
||||
// TailscaleVarRoot returns the root directory of Tailscale's writable
|
||||
// storage area. (e.g. "/var/lib/tailscale")
|
||||
//
|
||||
// It returns an empty string if there's no configured or discovered
|
||||
// location.
|
||||
func (b *LocalBackend) TailscaleVarRoot() string {
|
||||
if b.varRoot != "" {
|
||||
return b.varRoot
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android":
|
||||
dir, _ := paths.AppSharedDir.Load().(string)
|
||||
return dir
|
||||
}
|
||||
// Temporary (2021-09-27) transitional fix for #2927 (Synology
|
||||
// cert dir) on the way towards a more complete fix
|
||||
// (#2932). It fixes any case where the state file is provided
|
||||
// to tailscaled explicitly when it's not in the default
|
||||
// location.
|
||||
if fs, ok := b.store.(*ipn.FileStore); ok {
|
||||
if fp := fs.Path(); fp != "" {
|
||||
if dir := filepath.Dir(fp); strings.EqualFold(filepath.Base(dir), "tailscale") {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
}
|
||||
stateFile := paths.DefaultTailscaledStateFile()
|
||||
if stateFile == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Dir(stateFile)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
|
||||
@@ -2365,6 +2417,7 @@ func (b *LocalBackend) nextState() ipn.State {
|
||||
wantRunning = b.prefs.WantRunning
|
||||
loggedOut = b.prefs.LoggedOut
|
||||
st = b.engineStatus
|
||||
keyExpired = b.keyExpired
|
||||
)
|
||||
b.mu.Unlock()
|
||||
|
||||
@@ -2397,7 +2450,9 @@ func (b *LocalBackend) nextState() ipn.State {
|
||||
}
|
||||
case !wantRunning:
|
||||
return ipn.Stopped
|
||||
case !netMap.Expiry.IsZero() && time.Until(netMap.Expiry) <= 0:
|
||||
case keyExpired:
|
||||
// NetMap must be non-nil for us to get here.
|
||||
// The node key expired, need to relogin.
|
||||
return ipn.NeedsLogin
|
||||
case netMap.MachineStatus != tailcfg.MachineAuthorized:
|
||||
// TODO(crawshaw): handle tailcfg.MachineInvalid
|
||||
@@ -2478,6 +2533,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.userID = ""
|
||||
b.setNetMapLocked(nil)
|
||||
b.prefs = new(ipn.Prefs)
|
||||
b.keyExpired = false
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.activeLogin = ""
|
||||
@@ -2647,7 +2703,7 @@ func (b *LocalBackend) OperatorUserID() string {
|
||||
// TestOnlyPublicKeys returns the current machine and node public
|
||||
// keys. Used in tests only to facilitate automated node authorization
|
||||
// in the test harness.
|
||||
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeKey tailcfg.NodeKey) {
|
||||
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeKey key.NodePublic) {
|
||||
b.mu.Lock()
|
||||
prefs := b.prefs
|
||||
machinePrivKey := b.machinePrivKey
|
||||
@@ -2659,7 +2715,7 @@ func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeK
|
||||
|
||||
mk := machinePrivKey.Public()
|
||||
nk := prefs.Persist.PrivateNodeKey.Public()
|
||||
return mk, tailcfg.NodeKey(nk)
|
||||
return mk, nk
|
||||
}
|
||||
|
||||
func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
|
||||
@@ -2749,7 +2805,7 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
if prefs := b.prefs; prefs != nil {
|
||||
req.NodeKey = tailcfg.NodeKey(prefs.Persist.PrivateNodeKey.Public())
|
||||
req.NodeKey = prefs.Persist.PrivateNodeKey.Public()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if cc == nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -494,3 +495,103 @@ func TestFileTargets(t *testing.T) {
|
||||
}
|
||||
// (other cases handled by TestPeerAPIBase above)
|
||||
}
|
||||
|
||||
func TestInternalAndExternalInterfaces(t *testing.T) {
|
||||
type interfacePrefix struct {
|
||||
i interfaces.Interface
|
||||
pfx netaddr.IPPrefix
|
||||
}
|
||||
|
||||
masked := func(ips ...interfacePrefix) (pfxs []netaddr.IPPrefix) {
|
||||
for _, ip := range ips {
|
||||
pfxs = append(pfxs, ip.pfx.Masked())
|
||||
}
|
||||
return pfxs
|
||||
}
|
||||
iList := func(ips ...interfacePrefix) (il interfaces.List) {
|
||||
for _, ip := range ips {
|
||||
il = append(il, ip.i)
|
||||
}
|
||||
return il
|
||||
}
|
||||
newInterface := func(name, pfx string, wsl2, loopback bool) interfacePrefix {
|
||||
ippfx := netaddr.MustParseIPPrefix(pfx)
|
||||
ip := interfaces.Interface{
|
||||
Interface: &net.Interface{},
|
||||
AltAddrs: []net.Addr{
|
||||
ippfx.IPNet(),
|
||||
},
|
||||
}
|
||||
if loopback {
|
||||
ip.Flags = net.FlagLoopback
|
||||
}
|
||||
if wsl2 {
|
||||
ip.HardwareAddr = []byte{0x00, 0x15, 0x5d, 0x00, 0x00, 0x00}
|
||||
}
|
||||
return interfacePrefix{i: ip, pfx: ippfx}
|
||||
}
|
||||
var (
|
||||
en0 = newInterface("en0", "10.20.2.5/16", false, false)
|
||||
en1 = newInterface("en1", "192.168.1.237/24", false, false)
|
||||
wsl = newInterface("wsl", "192.168.5.34/24", true, false)
|
||||
loopback = newInterface("lo0", "127.0.0.1/8", false, true)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
goos string
|
||||
il interfaces.List
|
||||
wantInt []netaddr.IPPrefix
|
||||
wantExt []netaddr.IPPrefix
|
||||
}{
|
||||
{
|
||||
name: "single-interface",
|
||||
goos: "linux",
|
||||
il: iList(
|
||||
en0,
|
||||
loopback,
|
||||
),
|
||||
wantInt: masked(loopback),
|
||||
wantExt: masked(en0),
|
||||
},
|
||||
{
|
||||
name: "multiple-interfaces",
|
||||
goos: "linux",
|
||||
il: iList(
|
||||
en0,
|
||||
en1,
|
||||
wsl,
|
||||
loopback,
|
||||
),
|
||||
wantInt: masked(loopback),
|
||||
wantExt: masked(en0, en1, wsl),
|
||||
},
|
||||
{
|
||||
name: "wsl2",
|
||||
goos: "windows",
|
||||
il: iList(
|
||||
en0,
|
||||
en1,
|
||||
wsl,
|
||||
loopback,
|
||||
),
|
||||
wantInt: masked(loopback, wsl),
|
||||
wantExt: masked(en0, en1),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotInt, gotExt, err := internalAndExternalInterfacesFrom(tc.il, tc.goos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(gotInt, tc.wantInt) {
|
||||
t.Errorf("unexpected internal prefixes\ngot %v\nwant %v", gotInt, tc.wantInt)
|
||||
}
|
||||
if !reflect.DeepEqual(gotExt, tc.wantExt) {
|
||||
t.Errorf("unexpected external prefixes\ngot %v\nwant %v", gotExt, tc.wantExt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
TxBytes: 10,
|
||||
RxBytes: 10,
|
||||
LastHandshake: time.Now(),
|
||||
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
|
||||
NodeKey: key.NewNode().Public(),
|
||||
}},
|
||||
})
|
||||
lb.mu.Unlock()
|
||||
@@ -105,7 +105,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
TxBytes: 11,
|
||||
RxBytes: 12,
|
||||
LastHandshake: time.Now(),
|
||||
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
|
||||
NodeKey: key.NewNode().Public(),
|
||||
}},
|
||||
})
|
||||
lb.mu.Unlock()
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
@@ -29,11 +30,13 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
@@ -500,9 +503,16 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePeerPut(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v0/goroutines" {
|
||||
switch r.URL.Path {
|
||||
case "/v0/goroutines":
|
||||
h.handleServeGoroutines(w, r)
|
||||
return
|
||||
case "/v0/env":
|
||||
h.handleServeEnv(w, r)
|
||||
return
|
||||
case "/v0/metrics":
|
||||
h.handleServeMetrics(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
@@ -710,3 +720,32 @@ func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var data struct {
|
||||
Hostinfo *tailcfg.Hostinfo
|
||||
Uid int
|
||||
Args []string
|
||||
Env []string
|
||||
}
|
||||
data.Hostinfo = hostinfo.New()
|
||||
data.Uid = os.Getuid()
|
||||
data.Args = os.Args
|
||||
data.Env = os.Environ()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
@@ -122,7 +121,7 @@ func (cc *mockControl) populateKeys() (newKeys bool) {
|
||||
if cc.persist.PrivateNodeKey.IsZero() {
|
||||
cc.logf("Generating a new nodekey.")
|
||||
cc.persist.OldPrivateNodeKey = cc.persist.PrivateNodeKey
|
||||
cc.persist.PrivateNodeKey, _ = wgkey.NewPrivate()
|
||||
cc.persist.PrivateNodeKey = key.NewNode()
|
||||
newKeys = true
|
||||
}
|
||||
|
||||
@@ -137,9 +136,7 @@ func (cc *mockControl) send(err error, url string, loginFinished bool, nm *netma
|
||||
URL: url,
|
||||
NetMap: nm,
|
||||
Persist: &cc.persist,
|
||||
}
|
||||
if err != nil {
|
||||
s.Err = err.Error()
|
||||
Err: err,
|
||||
}
|
||||
if loginFinished {
|
||||
s.LoginFinished = &empty.Message{}
|
||||
@@ -870,6 +867,45 @@ func TestStateMachine(t *testing.T) {
|
||||
// change either.
|
||||
c.Assert(ipn.Starting, qt.Equals, b.State())
|
||||
}
|
||||
t.Logf("\n\nExpireKey")
|
||||
notifies.expect(1)
|
||||
cc.send(nil, "", false, &netmap.NetworkMap{
|
||||
Expiry: time.Now().Add(-time.Minute),
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
cc.assertCalls("unpause", "unpause")
|
||||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
c.Assert(b.isEngineBlocked(), qt.IsTrue)
|
||||
}
|
||||
|
||||
t.Logf("\n\nExtendKey")
|
||||
notifies.expect(1)
|
||||
cc.send(nil, "", false, &netmap.NetworkMap{
|
||||
Expiry: time.Now().Add(time.Minute),
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
cc.assertCalls("unpause", "unpause", "unpause")
|
||||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
c.Assert(ipn.Starting, qt.Equals, b.State())
|
||||
c.Assert(b.isEngineBlocked(), qt.IsFalse)
|
||||
}
|
||||
notifies.expect(1)
|
||||
// Fake a DERP connection.
|
||||
b.setWgengineStatus(&wgengine.Status{DERPs: 1}, nil)
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
cc.assertCalls("unpause")
|
||||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(ipn.Running, qt.Equals, *nn[0].State)
|
||||
c.Assert(ipn.Running, qt.Equals, b.State())
|
||||
}
|
||||
}
|
||||
|
||||
type testStateStorage struct {
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -53,16 +52,13 @@ import (
|
||||
|
||||
// Options is the configuration of the Tailscale node agent.
|
||||
type Options struct {
|
||||
// SocketPath, on unix systems, is the unix socket path to listen
|
||||
// on for frontend connections.
|
||||
SocketPath string
|
||||
|
||||
// Port, on windows, is the localhost TCP port to listen on for
|
||||
// frontend connections.
|
||||
Port int
|
||||
|
||||
// StatePath is the path to the stored agent state.
|
||||
StatePath string
|
||||
// VarRoot is the the Tailscale daemon's private writable
|
||||
// directory (usually "/var/lib/tailscale" on Linux) that
|
||||
// contains the "tailscaled.state" file, the "certs" directory
|
||||
// for TLS certs, and the "files" directory for incoming
|
||||
// Taildrop files before they're moved to a user directory.
|
||||
// If empty, Taildrop and TLS certs don't function.
|
||||
VarRoot string
|
||||
|
||||
// AutostartStateKey, if non-empty, immediately starts the agent
|
||||
// using the given StateKey. If empty, the agent stays idle and
|
||||
@@ -84,15 +80,11 @@ type Options struct {
|
||||
// the actual definition of "disconnect" is when the
|
||||
// connection count transitions from 1 to 0.
|
||||
SurviveDisconnects bool
|
||||
|
||||
// DebugMux, if non-nil, specifies an HTTP ServeMux in which
|
||||
// to register a debug handler.
|
||||
DebugMux *http.ServeMux
|
||||
}
|
||||
|
||||
// server is an IPN backend and its set of 0 or more active connections
|
||||
// talking to an IPN backend.
|
||||
type server struct {
|
||||
// Server is an IPN backend and its set of 0 or more active localhost
|
||||
// TCP or unix socket connections talking to that backend.
|
||||
type Server struct {
|
||||
b *ipnlocal.LocalBackend
|
||||
logf logger.Logf
|
||||
backendLogID string
|
||||
@@ -101,7 +93,8 @@ type server struct {
|
||||
// being run in "client mode" that requires an active GUI
|
||||
// connection (such as on Windows by default). Even if this
|
||||
// is true, the ForceDaemon pref can override this.
|
||||
resetOnZero bool
|
||||
resetOnZero bool
|
||||
autostartStateKey ipn.StateKey
|
||||
|
||||
bsMu sync.Mutex // lock order: bsMu, then mu
|
||||
bs *ipn.BackendServer
|
||||
@@ -114,6 +107,9 @@ type server struct {
|
||||
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
|
||||
}
|
||||
|
||||
// LocalBackend returns the server's LocalBackend.
|
||||
func (s *Server) LocalBackend() *ipnlocal.LocalBackend { return s.b }
|
||||
|
||||
// connIdentity represents the owner of a localhost TCP or unix socket connection.
|
||||
type connIdentity struct {
|
||||
Conn net.Conn
|
||||
@@ -135,7 +131,7 @@ type connIdentity struct {
|
||||
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
|
||||
// and a ConnIdentity with NotWindows set true. It's only an error if we expected
|
||||
// to be able to map it and couldn't.
|
||||
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
func (s *Server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
ci = connIdentity{Conn: c}
|
||||
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
||||
ci.NotWindows = true
|
||||
@@ -172,7 +168,7 @@ func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
return ci, fmt.Errorf("failed to map connection's pid to a user%s: %w", hint, err)
|
||||
}
|
||||
ci.UserID = uid
|
||||
u, err := s.lookupUserFromID(uid)
|
||||
u, err := lookupUserFromID(s.logf, uid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
|
||||
}
|
||||
@@ -180,10 +176,10 @@ func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func (s *server) lookupUserFromID(uid string) (*user.User, error) {
|
||||
func lookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
|
||||
s.logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
||||
logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
||||
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
||||
// now. We don't strictly need the username. It's just a nice-to-have.
|
||||
// So make up a *user.User if their machine is broken in this way.
|
||||
@@ -199,7 +195,7 @@ func (s *server) lookupUserFromID(uid string) (*user.User, error) {
|
||||
// blockWhileInUse blocks while until either a Read from conn fails
|
||||
// (i.e. it's closed) or until the server is able to accept ci as a
|
||||
// user.
|
||||
func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
|
||||
func (s *Server) blockWhileInUse(conn io.Reader, ci connIdentity) {
|
||||
s.logf("blocking client while server in use; connIdentity=%v", ci)
|
||||
connDone := make(chan struct{})
|
||||
go func() {
|
||||
@@ -241,7 +237,7 @@ func bufferHasHTTPRequest(br *bufio.Reader) bool {
|
||||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
|
||||
}
|
||||
|
||||
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
// First see if it's an HTTP request.
|
||||
br := bufio.NewReader(c)
|
||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||
@@ -391,7 +387,7 @@ func (e inUseOtherUserError) Unwrap() error { return e.error }
|
||||
// The returned error, when non-nil, will be of type inUseOtherUserError.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *server) checkConnIdentityLocked(ci connIdentity) error {
|
||||
func (s *Server) checkConnIdentityLocked(ci connIdentity) error {
|
||||
// If clients are already connected, verify they're the same user.
|
||||
// This mostly matters on Windows at the moment.
|
||||
if len(s.allClients) > 0 {
|
||||
@@ -413,14 +409,17 @@ func (s *server) checkConnIdentityLocked(ci connIdentity) error {
|
||||
// the Tailscale local daemon API.
|
||||
//
|
||||
// s.mu must not be held.
|
||||
func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||
if runtime.GOOS == "windows" {
|
||||
func (s *Server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.checkConnIdentityLocked(ci) == nil {
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
case "js":
|
||||
return true, true
|
||||
}
|
||||
if ci.IsUnixSock {
|
||||
return true, !isReadonlyConn(ci, s.b.OperatorUserID(), logger.Discard)
|
||||
@@ -430,7 +429,7 @@ func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||
|
||||
// registerDisconnectSub adds ch as a subscribe to connection disconnect
|
||||
// events. If add is false, the subscriber is removed.
|
||||
func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
||||
func (s *Server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if add {
|
||||
@@ -448,7 +447,7 @@ func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
||||
//
|
||||
// If the returned error is of type inUseOtherUserError then the
|
||||
// returned connIdentity is also valid.
|
||||
func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
func (s *Server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
ci, err = s.getConnIdentity(c)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -492,7 +491,7 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
func (s *Server) removeAndCloseConn(c net.Conn) {
|
||||
s.mu.Lock()
|
||||
delete(s.clients, c)
|
||||
delete(s.allClients, c)
|
||||
@@ -516,7 +515,7 @@ func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func (s *server) stopAll() {
|
||||
func (s *Server) stopAll() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for c := range s.clients {
|
||||
@@ -529,7 +528,7 @@ func (s *server) stopAll() {
|
||||
// setServerModeUserLocked is called when we're in server mode but our s.serverModeUser is nil.
|
||||
//
|
||||
// s.mu must be held
|
||||
func (s *server) setServerModeUserLocked() {
|
||||
func (s *Server) setServerModeUserLocked() {
|
||||
var ci connIdentity
|
||||
var ok bool
|
||||
for _, ci = range s.allClients {
|
||||
@@ -553,7 +552,7 @@ func (s *server) setServerModeUserLocked() {
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
func (s *server) writeToClients(n ipn.Notify) {
|
||||
func (s *Server) writeToClients(n ipn.Notify) {
|
||||
inServerMode := s.b.InServerMode()
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -602,27 +601,63 @@ func tryWindowsAppDataMigration(logf logger.Logf, path string) string {
|
||||
// what they are doing.
|
||||
return path
|
||||
}
|
||||
oldFile := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf")
|
||||
oldFile := paths.LegacyStateFilePath()
|
||||
return paths.TryConfigFileMigration(logf, oldFile, path)
|
||||
}
|
||||
|
||||
// StateStore returns a StateStore from path.
|
||||
//
|
||||
// The path should be an absolute path to a file.
|
||||
//
|
||||
// Special cases:
|
||||
//
|
||||
// * empty string means to use an in-memory store
|
||||
// * if the string begins with "kube:", the suffix
|
||||
// is a Kubernetes secret name
|
||||
// * if the string begins with "arn:", the value is
|
||||
// an AWS ARN for an SSM.
|
||||
func StateStore(path string, logf logger.Logf) (ipn.StateStore, error) {
|
||||
if path == "" {
|
||||
return &ipn.MemoryStore{}, nil
|
||||
}
|
||||
const kubePrefix = "kube:"
|
||||
const arnPrefix = "arn:"
|
||||
switch {
|
||||
case strings.HasPrefix(path, kubePrefix):
|
||||
secretName := strings.TrimPrefix(path, kubePrefix)
|
||||
store, err := ipn.NewKubeStore(secretName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ipn.NewKubeStore(%q): %v", secretName, err)
|
||||
}
|
||||
return store, nil
|
||||
case strings.HasPrefix(path, arnPrefix):
|
||||
store, err := aws.NewStore(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aws.NewStore(%q): %v", path, err)
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
path = tryWindowsAppDataMigration(logf, path)
|
||||
}
|
||||
store, err := ipn.NewFileStore(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ipn.NewFileStore(%q): %v", path, err)
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// Run runs a Tailscale backend service.
|
||||
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
|
||||
func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
//
|
||||
// Deprecated: use New and Server.Run instead.
|
||||
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
|
||||
listen, _, err := safesocket.Listen(opts.SocketPath, uint16(opts.Port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
server := &server{
|
||||
backendLogID: logid,
|
||||
logf: logf,
|
||||
resetOnZero: !opts.SurviveDisconnects,
|
||||
}
|
||||
var serverMu sync.Mutex
|
||||
var serverOrNil *Server
|
||||
|
||||
// When the context is closed or when we return, whichever is first, close our listener
|
||||
// and all open connections.
|
||||
@@ -631,57 +666,33 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
case <-ctx.Done():
|
||||
case <-runDone:
|
||||
}
|
||||
server.stopAll()
|
||||
listen.Close()
|
||||
serverMu.Lock()
|
||||
if s := serverOrNil; s != nil {
|
||||
s.stopAll()
|
||||
}
|
||||
serverMu.Unlock()
|
||||
ln.Close()
|
||||
}()
|
||||
logf("Listening on %v", listen.Addr())
|
||||
logf("Listening on %v", ln.Addr())
|
||||
|
||||
var store ipn.StateStore
|
||||
if opts.StatePath != "" {
|
||||
const kubePrefix = "kube:"
|
||||
const arnPrefix = "arn:"
|
||||
path := opts.StatePath
|
||||
switch {
|
||||
case strings.HasPrefix(path, kubePrefix):
|
||||
secretName := strings.TrimPrefix(path, kubePrefix)
|
||||
store, err = ipn.NewKubeStore(secretName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipn.NewKubeStore(%q): %v", secretName, err)
|
||||
}
|
||||
case strings.HasPrefix(path, arnPrefix):
|
||||
store, err = aws.NewStore(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("aws.NewStore(%q): %v", path, err)
|
||||
}
|
||||
default:
|
||||
if runtime.GOOS == "windows" {
|
||||
path = tryWindowsAppDataMigration(logf, path)
|
||||
}
|
||||
store, err = ipn.NewFileStore(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipn.NewFileStore(%q): %v", path, err)
|
||||
}
|
||||
var serverModeUser *user.User
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
if err != nil && err != ipn.ErrStateNotExist {
|
||||
return fmt.Errorf("calling ReadState on state store: %w", err)
|
||||
}
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
if err != nil && err != ipn.ErrStateNotExist {
|
||||
return fmt.Errorf("calling ReadState on %s: %w", path, err)
|
||||
}
|
||||
key := string(autoStartKey)
|
||||
if strings.HasPrefix(key, "user-") {
|
||||
uid := strings.TrimPrefix(key, "user-")
|
||||
u, err := server.lookupUserFromID(uid)
|
||||
if err != nil {
|
||||
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
|
||||
} else {
|
||||
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
|
||||
server.serverModeUser = u
|
||||
}
|
||||
opts.AutostartStateKey = ipn.StateKey(key)
|
||||
key := string(autoStartKey)
|
||||
if strings.HasPrefix(key, "user-") {
|
||||
uid := strings.TrimPrefix(key, "user-")
|
||||
u, err := lookupUserFromID(logf, uid)
|
||||
if err != nil {
|
||||
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
|
||||
} else {
|
||||
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
|
||||
serverModeUser = u
|
||||
}
|
||||
opts.AutostartStateKey = ipn.StateKey(key)
|
||||
}
|
||||
} else {
|
||||
store = &ipn.MemoryStore{}
|
||||
}
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
@@ -691,7 +702,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
@@ -717,54 +728,115 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
return err
|
||||
}
|
||||
}
|
||||
if unservedConn != nil {
|
||||
ln = &listenerWithReadyConn{
|
||||
Listener: ln,
|
||||
c: unservedConn,
|
||||
}
|
||||
}
|
||||
|
||||
server, err := New(logf, logid, store, eng, serverModeUser, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serverMu.Lock()
|
||||
serverOrNil = server
|
||||
serverMu.Unlock()
|
||||
return server.Run(ctx, ln)
|
||||
}
|
||||
|
||||
// New returns a new Server.
|
||||
//
|
||||
// To start it, use the Server.Run method.
|
||||
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, serverModeUser *user.User, opts Options) (*Server, error) {
|
||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
return nil, fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
defer b.Shutdown()
|
||||
b.SetVarRoot(opts.VarRoot)
|
||||
b.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
|
||||
if opts.DebugMux != nil {
|
||||
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
|
||||
serveHTMLStatus(w, b)
|
||||
})
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
if err != nil && err != ipn.ErrStateNotExist {
|
||||
return nil, fmt.Errorf("calling ReadState on store: %w", err)
|
||||
}
|
||||
key := string(autoStartKey)
|
||||
if strings.HasPrefix(key, "user-") {
|
||||
uid := strings.TrimPrefix(key, "user-")
|
||||
u, err := lookupUserFromID(logf, uid)
|
||||
if err != nil {
|
||||
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
|
||||
} else {
|
||||
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
|
||||
serverModeUser = u
|
||||
}
|
||||
opts.AutostartStateKey = ipn.StateKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
server.b = b
|
||||
server := &Server{
|
||||
b: b,
|
||||
backendLogID: logid,
|
||||
logf: logf,
|
||||
resetOnZero: !opts.SurviveDisconnects,
|
||||
serverModeUser: serverModeUser,
|
||||
autostartStateKey: opts.AutostartStateKey,
|
||||
}
|
||||
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
||||
return server, nil
|
||||
}
|
||||
|
||||
if opts.AutostartStateKey != "" {
|
||||
server.bs.GotCommand(context.TODO(), &ipn.Command{
|
||||
// Run runs the server, accepting connections from ln forever.
|
||||
//
|
||||
// If the context is done, the listener is closed.
|
||||
func (s *Server) Run(ctx context.Context, ln net.Listener) error {
|
||||
defer s.b.Shutdown()
|
||||
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
|
||||
// When the context is closed or when we return, whichever is first, close our listener
|
||||
// and all open connections.
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-runDone:
|
||||
}
|
||||
s.stopAll()
|
||||
ln.Close()
|
||||
}()
|
||||
|
||||
if s.autostartStateKey != "" {
|
||||
s.bs.GotCommand(ctx, &ipn.Command{
|
||||
Version: version.Long,
|
||||
Start: &ipn.StartArgs{
|
||||
Opts: ipn.Options{StateKey: opts.AutostartStateKey},
|
||||
Opts: ipn.Options{StateKey: s.autostartStateKey},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
systemd.Ready()
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
var c net.Conn
|
||||
var err error
|
||||
if unservedConn != nil {
|
||||
c = unservedConn
|
||||
unservedConn = nil
|
||||
} else {
|
||||
c, err = listen.Accept()
|
||||
bo := backoff.NewBackoff("ipnserver", s.logf, 30*time.Second)
|
||||
var connNum int
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
logf("ipnserver: Accept: %v", err)
|
||||
bo.BackOff(ctx, err)
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
s.logf("ipnserver: Accept: %v", err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
go server.serveConn(ctx, c, logger.WithPrefix(logf, fmt.Sprintf("ipnserver: conn%d: ", i)))
|
||||
connNum++
|
||||
go s.serveConn(ctx, c, logger.WithPrefix(s.logf, fmt.Sprintf("ipnserver: conn%d: ", connNum)))
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// BabysitProc runs the current executable as a child process with the
|
||||
@@ -954,7 +1026,7 @@ func (a dummyAddr) String() string { return string(a) }
|
||||
// HTTP. So we Read from its bufio.Reader. On Close, we we tell the
|
||||
// server it's closed, so the server can account the who's connected.
|
||||
type protoSwitchConn struct {
|
||||
s *server
|
||||
s *Server
|
||||
net.Conn
|
||||
br *bufio.Reader
|
||||
closeOnce sync.Once
|
||||
@@ -966,7 +1038,7 @@ func (psc *protoSwitchConn) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
func (s *Server) localhostHandler(ci connIdentity) http.Handler {
|
||||
lah := localapi.NewHandler(s.b, s.logf, s.backendLogID)
|
||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||
|
||||
@@ -979,13 +1051,13 @@ func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||
return
|
||||
}
|
||||
serveHTMLStatus(w, s.b)
|
||||
s.ServeHTMLStatus(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func serveHTMLStatus(w http.ResponseWriter, b *ipnlocal.LocalBackend) {
|
||||
func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := b.Status()
|
||||
st := s.b.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
st.WriteHTML(w)
|
||||
}
|
||||
@@ -1020,3 +1092,23 @@ func marshalNotify(n ipn.Notify, logf logger.Logf) (b []byte, ok bool) {
|
||||
}
|
||||
return b, true
|
||||
}
|
||||
|
||||
// listenerWithReadyConn is a net.Listener wrapper that has
|
||||
// one net.Conn ready to be accepted already.
|
||||
type listenerWithReadyConn struct {
|
||||
net.Listener
|
||||
|
||||
mu sync.Mutex
|
||||
c net.Conn // if non-nil, ready to be Accepted
|
||||
}
|
||||
|
||||
func (ln *listenerWithReadyConn) Accept() (net.Conn, error) {
|
||||
ln.mu.Lock()
|
||||
c := ln.c
|
||||
ln.c = nil
|
||||
ln.mu.Unlock()
|
||||
if c != nil {
|
||||
return c, nil
|
||||
}
|
||||
return ln.Listener.Accept()
|
||||
}
|
||||
|
||||
@@ -62,10 +62,16 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(eng.Close)
|
||||
|
||||
opts := ipnserver.Options{
|
||||
SocketPath: socketPath,
|
||||
}
|
||||
opts := ipnserver.Options{}
|
||||
t.Logf("pre-Run")
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
store := new(ipn.MemoryStore)
|
||||
|
||||
ln, _, err := safesocket.Listen(socketPath, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
t.Logf("ipnserver.Run = %v", err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
package ipnstate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
@@ -57,28 +56,28 @@ type Status struct {
|
||||
// trailing periods, and without any "_acme-challenge." prefix.
|
||||
CertDomains []string
|
||||
|
||||
Peer map[key.Public]*PeerStatus
|
||||
Peer map[key.NodePublic]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
}
|
||||
|
||||
func (s *Status) Peers() []key.Public {
|
||||
kk := make([]key.Public, 0, len(s.Peer))
|
||||
func (s *Status) Peers() []key.NodePublic {
|
||||
kk := make([]key.NodePublic, 0, len(s.Peer))
|
||||
for k := range s.Peer {
|
||||
kk = append(kk, k)
|
||||
}
|
||||
sort.Slice(kk, func(i, j int) bool { return bytes.Compare(kk[i][:], kk[j][:]) < 0 })
|
||||
sort.Slice(kk, func(i, j int) bool { return kk[i].Less(kk[j]) })
|
||||
return kk
|
||||
}
|
||||
|
||||
type PeerStatusLite struct {
|
||||
TxBytes, RxBytes int64
|
||||
LastHandshake time.Time
|
||||
NodeKey tailcfg.NodeKey
|
||||
NodeKey key.NodePublic
|
||||
}
|
||||
|
||||
type PeerStatus struct {
|
||||
ID tailcfg.StableNodeID
|
||||
PublicKey key.Public
|
||||
PublicKey key.NodePublic
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
DNSName string
|
||||
OS string // HostInfo.OS
|
||||
@@ -201,7 +200,7 @@ func (sb *StatusBuilder) AddTailscaleIP(ip netaddr.IP) {
|
||||
// AddPeer adds a peer node to the status.
|
||||
//
|
||||
// Its PeerStatus is mixed with any previous status already added.
|
||||
func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if st == nil {
|
||||
panic("nil PeerStatus")
|
||||
}
|
||||
@@ -214,7 +213,7 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
}
|
||||
|
||||
if sb.st.Peer == nil {
|
||||
sb.st.Peer = make(map[key.Public]*PeerStatus)
|
||||
sb.st.Peer = make(map[key.NodePublic]*PeerStatus)
|
||||
}
|
||||
e, ok := sb.st.Peer[peer]
|
||||
if !ok {
|
||||
@@ -478,5 +477,6 @@ func sortKey(ps *PeerStatus) string {
|
||||
if len(ps.TailscaleIPs) > 0 {
|
||||
return ps.TailscaleIPs[0].String()
|
||||
}
|
||||
return string(ps.PublicKey[:])
|
||||
raw := ps.PublicKey.Raw32()
|
||||
return string(raw[:])
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -113,6 +114,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveSetDNS(w, r)
|
||||
case "/localapi/v0/derpmap":
|
||||
h.serveDERPMap(w, r)
|
||||
case "/localapi/v0/metrics":
|
||||
h.serveMetrics(w, r)
|
||||
case "/":
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
default:
|
||||
@@ -184,6 +187,17 @@ func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
// Require write access out of paranoia that the metrics
|
||||
// might contain something sensitive.
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "metric access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
|
||||
// for platforms where we want to link it in.
|
||||
var serveProfileFunc func(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by the following command; DO NOT EDIT.
|
||||
// tailscale.com/cmd/cloner -type Prefs
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
|
||||
package ipn
|
||||
|
||||
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
@@ -404,7 +405,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
{
|
||||
Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: wgkey.Private{1: 1},
|
||||
PrivateNodeKey: key.NodePrivateFromRaw32(mem.B([]byte{1: 1, 31: 0})),
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
|
||||
@@ -28,7 +28,7 @@ var ErrStateNotExist = errors.New("no state with given ID")
|
||||
|
||||
const (
|
||||
// MachineKeyStateKey is the key under which we store the machine key,
|
||||
// in its wgkey.Private.MarshalText representation.
|
||||
// in its key.NodePrivate.MarshalText representation.
|
||||
MachineKeyStateKey = StateKey("_machinekey")
|
||||
|
||||
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
|
||||
|
||||
@@ -106,6 +106,7 @@ func (w *logFileWriter) appendToFileLocked(out []byte) {
|
||||
if w.fday != day {
|
||||
w.startNewFileLocked()
|
||||
}
|
||||
out = removeDatePrefix(out)
|
||||
if w.f != nil {
|
||||
// RFC3339Nano but with a fixed number (3) of nanosecond digits:
|
||||
const formatPre = "2006-01-02T15:04:05"
|
||||
@@ -118,6 +119,30 @@ func (w *logFileWriter) appendToFileLocked(out []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func isNum(b byte) bool { return '0' <= b && b <= '9' }
|
||||
|
||||
// removeDatePrefix returns a subslice of v with the log package's
|
||||
// standard datetime prefix format removed, if present.
|
||||
func removeDatePrefix(v []byte) []byte {
|
||||
const format = "2009/01/23 01:23:23 "
|
||||
if len(v) < len(format) {
|
||||
return v
|
||||
}
|
||||
for i, b := range v[:len(format)] {
|
||||
fb := format[i]
|
||||
if isNum(fb) {
|
||||
if !isNum(b) {
|
||||
return v
|
||||
}
|
||||
continue
|
||||
}
|
||||
if b != fb {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return v[len(format):]
|
||||
}
|
||||
|
||||
// startNewFileLocked opens a new log file for writing
|
||||
// and also cleans up any old files.
|
||||
//
|
||||
|
||||
28
log/filelogger/log_test.go
Normal file
28
log/filelogger/log_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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 filelogger
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRemoveDatePrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"\n", "\n"},
|
||||
{"2009/01/23 01:23:23", "2009/01/23 01:23:23"},
|
||||
{"2009/01/23 01:23:23 \n", "\n"},
|
||||
{"2009/01/23 01:23:23 foo\n", "foo\n"},
|
||||
{"9999/01/23 01:23:23 foo\n", "foo\n"},
|
||||
{"2009_01/23 01:23:23 had an underscore\n", "2009_01/23 01:23:23 had an underscore\n"},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := removeDatePrefix([]byte(tt.in))
|
||||
if string(got) != tt.want {
|
||||
t.Logf("[%d] removeDatePrefix(%q) = %q; want %q", i, tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,6 +31,8 @@ import (
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
@@ -38,6 +40,7 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/racebuild"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
@@ -500,6 +503,9 @@ func New(collection string) *Policy {
|
||||
},
|
||||
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
|
||||
}
|
||||
if collection == logtail.CollectionNode {
|
||||
c.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta
|
||||
}
|
||||
|
||||
if val := getLogTarget(); val != "" {
|
||||
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
|
||||
@@ -581,17 +587,29 @@ func newLogtailTransport(host string) *http.Transport {
|
||||
|
||||
// Log whenever we dial:
|
||||
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||
nd := netns.FromDialer(&net.Dialer{
|
||||
nd := netns.FromDialer(log.Printf, &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: netknob.PlatformTCPKeepAlive(),
|
||||
})
|
||||
t0 := time.Now()
|
||||
c, err := nd.DialContext(ctx, netw, addr)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
log.Printf("logtail: dial %q failed: %v (in %v)", addr, err, d)
|
||||
} else {
|
||||
if err == nil {
|
||||
log.Printf("logtail: dialed %q in %v", addr, d)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// If we failed to dial, try again with bootstrap DNS.
|
||||
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
}
|
||||
dialer := dnscache.Dialer(nd.DialContext, dnsCache)
|
||||
c, err = dialer(ctx, netw, addr)
|
||||
if err == nil {
|
||||
log.Printf("logtail: bootstrap dial succeeded")
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@ import (
|
||||
|
||||
var stderrFD = 2 // a variable for testing
|
||||
|
||||
const defaultMaxFileSize = 50 << 20
|
||||
|
||||
type Options struct {
|
||||
ReplaceStderr bool // dup over fd 2 so everything written to stderr comes here
|
||||
MaxFileSize int
|
||||
}
|
||||
|
||||
// A Filch uses two alternating files as a simplistic ring buffer.
|
||||
@@ -30,6 +33,10 @@ type Filch struct {
|
||||
alt *os.File
|
||||
altscan *bufio.Scanner
|
||||
recovered int64
|
||||
|
||||
maxFileSize int64
|
||||
writeCounter int
|
||||
|
||||
// buf is an initial buffer for altscan.
|
||||
// As of August 2021, 99.96% of all log lines
|
||||
// are below 4096 bytes in length.
|
||||
@@ -38,7 +45,7 @@ type Filch struct {
|
||||
// so that the whole struct takes 4096 bytes
|
||||
// (less on 32 bit platforms).
|
||||
// This reduces allocation waste.
|
||||
buf [4096 - 48]byte
|
||||
buf [4096 - 64]byte
|
||||
}
|
||||
|
||||
// TryReadline implements the logtail.Buffer interface.
|
||||
@@ -91,6 +98,22 @@ func (f *Filch) scan() ([]byte, error) {
|
||||
func (f *Filch) Write(b []byte) (int, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.writeCounter == 100 {
|
||||
// Check the file size every 100 writes.
|
||||
f.writeCounter = 0
|
||||
fi, err := f.cur.Stat()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if fi.Size() >= f.maxFileSize {
|
||||
// This most likely means we are not draining.
|
||||
// To limit the amount of space we use, throw away the old logs.
|
||||
if err := moveContents(f.alt, f.cur); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
f.writeCounter++
|
||||
|
||||
if len(b) == 0 || b[len(b)-1] != '\n' {
|
||||
bnl := make([]byte, len(b)+1)
|
||||
@@ -159,8 +182,13 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mfs := defaultMaxFileSize
|
||||
if opts.MaxFileSize > 0 {
|
||||
mfs = opts.MaxFileSize
|
||||
}
|
||||
f = &Filch{
|
||||
OrigStderr: os.Stderr, // temporary, for past logs recovery
|
||||
OrigStderr: os.Stderr, // temporary, for past logs recovery
|
||||
maxFileSize: int64(mfs),
|
||||
}
|
||||
|
||||
// Neither, either, or both files may exist and contain logs from
|
||||
@@ -234,6 +262,9 @@ func moveContents(dst, src *os.File) (err error) {
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := dst.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -57,6 +57,39 @@ func (f *filchTest) close(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropOldLogs(t *testing.T) {
|
||||
const line1 = "123456789" // 10 bytes (9+newline)
|
||||
tests := []struct {
|
||||
write, read int
|
||||
}{
|
||||
{10, 10},
|
||||
{100, 100},
|
||||
{200, 200},
|
||||
{250, 150},
|
||||
{500, 200},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(fmt.Sprintf("w%d-r%d", tc.write, tc.read), func(t *testing.T) {
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false, MaxFileSize: 1000})
|
||||
defer f.close(t)
|
||||
// Make filch rotate the logs 3 times
|
||||
for i := 0; i < tc.write; i++ {
|
||||
f.write(t, line1)
|
||||
}
|
||||
// We should only be able to read the last 150 lines
|
||||
for i := 0; i < tc.read; i++ {
|
||||
f.read(t, line1)
|
||||
if t.Failed() {
|
||||
t.Logf("could only read %d lines", i)
|
||||
break
|
||||
}
|
||||
}
|
||||
f.readEOF(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
|
||||
@@ -28,6 +28,12 @@ import (
|
||||
// Config.BaseURL isn't provided.
|
||||
const DefaultHost = "log.tailscale.io"
|
||||
|
||||
const (
|
||||
// CollectionNode is the name of a logtail Config.Collection
|
||||
// for tailscaled (or equivalent: IPNExtension, Android app).
|
||||
CollectionNode = "tailnode.log.tailscale.io"
|
||||
)
|
||||
|
||||
type Encoder interface {
|
||||
EncodeAll(src, dst []byte) []byte
|
||||
Close() error
|
||||
@@ -46,6 +52,12 @@ type Config struct {
|
||||
Buffer Buffer // temp storage, if nil a MemoryBuffer
|
||||
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
|
||||
|
||||
// MetricsDelta, if non-nil, is a func that returns an encoding
|
||||
// delta in clientmetrics to upload alongside existing logs.
|
||||
// It can return either an empty string (for nothing) or a string
|
||||
// that's safe to embed in a JSON string literal without further escaping.
|
||||
MetricsDelta func() string
|
||||
|
||||
// DrainLogs, if non-nil, disables automatic uploading of new logs,
|
||||
// so that logs are only uploaded when a token is sent to DrainLogs.
|
||||
DrainLogs <-chan struct{}
|
||||
@@ -84,6 +96,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
drainLogs: cfg.DrainLogs,
|
||||
timeNow: cfg.TimeNow,
|
||||
bo: backoff.NewBackoff("logtail", logf, 30*time.Second),
|
||||
metricsDelta: cfg.MetricsDelta,
|
||||
|
||||
shutdownStart: make(chan struct{}),
|
||||
shutdownDone: make(chan struct{}),
|
||||
@@ -119,6 +132,7 @@ type Logger struct {
|
||||
zstdEncoder Encoder
|
||||
uploadCancel func()
|
||||
explainedRaw bool
|
||||
metricsDelta func() string // or nil
|
||||
|
||||
shutdownStart chan struct{} // closed when shutdown begins
|
||||
shutdownDone chan struct{} // closed when shutdown complete
|
||||
@@ -426,6 +440,14 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
b = append(b, "\"}, "...)
|
||||
}
|
||||
|
||||
if l.metricsDelta != nil {
|
||||
if d := l.metricsDelta(); d != "" {
|
||||
b = append(b, `"metrics": "`...)
|
||||
b = append(b, d...)
|
||||
b = append(b, `",`...)
|
||||
}
|
||||
}
|
||||
|
||||
b = append(b, "\"text\": \""...)
|
||||
for i, c := range buf {
|
||||
switch c {
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestFastShutdown(t *testing.T) {
|
||||
@@ -213,11 +215,11 @@ var sink []byte
|
||||
func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
lg := &Logger{timeNow: time.Now}
|
||||
inBuf := []byte("some text to encode")
|
||||
n := testing.AllocsPerRun(1000, func() {
|
||||
err := tstest.MinAllocsPerRun(t, 1, func() {
|
||||
sink = lg.encodeText(inBuf, false)
|
||||
})
|
||||
if int(n) != 1 {
|
||||
t.Logf("allocs = %d; want 1", int(n))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,15 +300,14 @@ func TestPublicIDUnmarshalText(t *testing.T) {
|
||||
if id.String() != hexStr {
|
||||
t.Errorf("String = %q; want %q", id.String(), hexStr)
|
||||
}
|
||||
|
||||
n := int(testing.AllocsPerRun(1000, func() {
|
||||
err := tstest.MinAllocsPerRun(t, 0, func() {
|
||||
var id PublicID
|
||||
if err := id.UnmarshalText(x); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}))
|
||||
if n != 0 {
|
||||
t.Errorf("allocs = %v; want 0", n)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestCurrentFileDescriptors(t *testing.T) {
|
||||
@@ -19,11 +21,11 @@ func TestCurrentFileDescriptors(t *testing.T) {
|
||||
t.Fatalf("got %v; want >= 3", n)
|
||||
}
|
||||
|
||||
allocs := int(testing.AllocsPerRun(100, func() {
|
||||
err := tstest.MinAllocsPerRun(t, 0, func() {
|
||||
n = CurrentFDs()
|
||||
}))
|
||||
if allocs != 0 {
|
||||
t.Errorf("allocs = %v; want 0", allocs)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Open some FDs.
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -76,28 +77,16 @@ func readResolv(r io.Reader) (config OSConfig, err error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (m directManager) readResolvFile(path string) (OSConfig, error) {
|
||||
b, err := m.fs.ReadFile(path)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
return readResolv(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// readResolvConf reads DNS configuration from /etc/resolv.conf.
|
||||
func (m directManager) readResolvConf() (OSConfig, error) {
|
||||
return m.readResolvFile(resolvConf)
|
||||
}
|
||||
|
||||
// resolvOwner returns the apparent owner of the resolv.conf
|
||||
// configuration in bs - one of "resolvconf", "systemd-resolved" or
|
||||
// "NetworkManager", or "" if no known owner was found.
|
||||
func resolvOwner(bs []byte) string {
|
||||
likely := ""
|
||||
b := bytes.NewBuffer(bs)
|
||||
for {
|
||||
line, err := b.ReadString('\n')
|
||||
if err != nil {
|
||||
return ""
|
||||
return likely
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
@@ -106,15 +95,15 @@ func resolvOwner(bs []byte) string {
|
||||
if line[0] != '#' {
|
||||
// First non-empty, non-comment line. Assume the owner
|
||||
// isn't hiding further down.
|
||||
return ""
|
||||
return likely
|
||||
}
|
||||
|
||||
if strings.Contains(line, "systemd-resolved") {
|
||||
return "systemd-resolved"
|
||||
likely = "systemd-resolved"
|
||||
} else if strings.Contains(line, "NetworkManager") {
|
||||
return "NetworkManager"
|
||||
likely = "NetworkManager"
|
||||
} else if strings.Contains(line, "resolvconf") {
|
||||
return "resolvconf"
|
||||
likely = "resolvconf"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,20 +135,46 @@ func isResolvedRunning() bool {
|
||||
// The caller must call Down before program shutdown
|
||||
// or as cleanup if the program terminates unexpectedly.
|
||||
type directManager struct {
|
||||
fs wholeFileFS
|
||||
logf logger.Logf
|
||||
fs wholeFileFS
|
||||
// renameBroken is set if fs.Rename to or from /etc/resolv.conf
|
||||
// fails. This can happen in some container runtimes, where
|
||||
// /etc/resolv.conf is bind-mounted from outside the container,
|
||||
// and therefore /etc and /etc/resolv.conf are different
|
||||
// filesystems as far as rename(2) is concerned.
|
||||
//
|
||||
// In those situations, we fall back to emulating rename with file
|
||||
// copies and truncations, which is not as good (opens up a race
|
||||
// where a reader can see an empty or partial /etc/resolv.conf),
|
||||
// but is better than having non-functioning DNS.
|
||||
renameBroken bool
|
||||
}
|
||||
|
||||
func newDirectManager() directManager {
|
||||
return directManager{fs: directFS{}}
|
||||
func newDirectManager(logf logger.Logf) *directManager {
|
||||
return &directManager{
|
||||
logf: logf,
|
||||
fs: directFS{},
|
||||
}
|
||||
}
|
||||
|
||||
func newDirectManagerOnFS(fs wholeFileFS) directManager {
|
||||
return directManager{fs: fs}
|
||||
func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager {
|
||||
return &directManager{
|
||||
logf: logf,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *directManager) readResolvFile(path string) (OSConfig, error) {
|
||||
b, err := m.fs.ReadFile(path)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
return readResolv(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
|
||||
// tailscale-managed file.
|
||||
func (m directManager) ownedByTailscale() (bool, error) {
|
||||
func (m *directManager) ownedByTailscale() (bool, error) {
|
||||
isRegular, err := m.fs.Stat(resolvConf)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -182,7 +197,7 @@ func (m directManager) ownedByTailscale() (bool, error) {
|
||||
|
||||
// backupConfig creates or updates a backup of /etc/resolv.conf, if
|
||||
// resolv.conf does not currently contain a Tailscale-managed config.
|
||||
func (m directManager) backupConfig() error {
|
||||
func (m *directManager) backupConfig() error {
|
||||
if _, err := m.fs.Stat(resolvConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No resolv.conf, nothing to back up. Also get rid of any
|
||||
@@ -201,10 +216,10 @@ func (m directManager) backupConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.fs.Rename(resolvConf, backupConf)
|
||||
return m.rename(resolvConf, backupConf)
|
||||
}
|
||||
|
||||
func (m directManager) restoreBackup() (restored bool, err error) {
|
||||
func (m *directManager) restoreBackup() (restored bool, err error) {
|
||||
if _, err := m.fs.Stat(backupConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No backup, nothing we can do.
|
||||
@@ -230,14 +245,48 @@ func (m directManager) restoreBackup() (restored bool, err error) {
|
||||
}
|
||||
|
||||
// We own resolv.conf, and a backup exists.
|
||||
if err := m.fs.Rename(backupConf, resolvConf); err != nil {
|
||||
if err := m.rename(backupConf, resolvConf); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m directManager) SetDNS(config OSConfig) (err error) {
|
||||
// rename tries to rename old to new using m.fs.Rename, and falls back
|
||||
// to hand-copying bytes and truncating old if that fails.
|
||||
//
|
||||
// This is a workaround to /etc/resolv.conf being a bind-mounted file
|
||||
// some container environments, which cannot be moved elsewhere in
|
||||
// /etc (because that would be a cross-filesystem move) or deleted
|
||||
// (because that would break the bind in surprising ways).
|
||||
func (m *directManager) rename(old, new string) error {
|
||||
if !m.renameBroken {
|
||||
err := m.fs.Rename(old, new)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
m.logf("rename of %q to %q failed (%v), falling back to copy+delete", old, new, err)
|
||||
m.renameBroken = true
|
||||
}
|
||||
|
||||
bs, err := m.fs.ReadFile(old)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %q to rename: %v", old, err)
|
||||
}
|
||||
if err := m.fs.WriteFile(new, bs, 0644); err != nil {
|
||||
return fmt.Errorf("writing to %q in rename of %q: %v", new, old, err)
|
||||
}
|
||||
|
||||
if err := m.fs.Remove(old); err != nil {
|
||||
err2 := m.fs.Truncate(old)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("remove of %q failed (%v) and so did truncate: %v", old, err, err2)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
var changed bool
|
||||
if config.IsZero() {
|
||||
changed, err = m.restoreBackup()
|
||||
@@ -252,7 +301,7 @@ func (m directManager) SetDNS(config OSConfig) (err error) {
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
if err := atomicWriteFile(m.fs, resolvConf, buf.Bytes(), 0644); err != nil {
|
||||
if err := m.atomicWriteFile(m.fs, resolvConf, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -280,11 +329,11 @@ func (m directManager) SetDNS(config OSConfig) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m directManager) SupportsSplitDNS() bool {
|
||||
func (m *directManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m directManager) GetBaseConfig() (OSConfig, error) {
|
||||
func (m *directManager) GetBaseConfig() (OSConfig, error) {
|
||||
owned, err := m.ownedByTailscale()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
@@ -297,7 +346,7 @@ func (m directManager) GetBaseConfig() (OSConfig, error) {
|
||||
return m.readResolvFile(fileToRead)
|
||||
}
|
||||
|
||||
func (m directManager) Close() error {
|
||||
func (m *directManager) Close() error {
|
||||
// We used to keep a file for the tailscale config and symlinked
|
||||
// to it, but then we stopped because /etc/resolv.conf being a
|
||||
// symlink to surprising places breaks snaps and other sandboxing
|
||||
@@ -329,7 +378,7 @@ func (m directManager) Close() error {
|
||||
}
|
||||
|
||||
// We own resolv.conf, and a backup exists.
|
||||
if err := m.fs.Rename(backupConf, resolvConf); err != nil {
|
||||
if err := m.rename(backupConf, resolvConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -340,7 +389,7 @@ func (m directManager) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func atomicWriteFile(fs wholeFileFS, filename string, data []byte, perm os.FileMode) error {
|
||||
func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []byte, perm os.FileMode) error {
|
||||
var randBytes [12]byte
|
||||
if _, err := rand.Read(randBytes[:]); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: %w", err)
|
||||
@@ -352,7 +401,7 @@ func atomicWriteFile(fs wholeFileFS, filename string, data []byte, perm os.FileM
|
||||
if err := fs.WriteFile(tmpName, data, perm); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: %w", err)
|
||||
}
|
||||
return fs.Rename(tmpName, filename)
|
||||
return m.rename(tmpName, filename)
|
||||
}
|
||||
|
||||
// wholeFileFS is a high-level file system abstraction designed just for use
|
||||
@@ -364,6 +413,7 @@ type wholeFileFS interface {
|
||||
Rename(oldName, newName string) error
|
||||
Remove(name string) error
|
||||
ReadFile(name string) ([]byte, error)
|
||||
Truncate(name string) error
|
||||
WriteFile(name string, contents []byte, perm os.FileMode) error
|
||||
}
|
||||
|
||||
@@ -396,6 +446,10 @@ func (fs directFS) ReadFile(name string) ([]byte, error) {
|
||||
return ioutil.ReadFile(fs.path(name))
|
||||
}
|
||||
|
||||
func (fs directFS) Truncate(name string) error {
|
||||
return os.Truncate(fs.path(name), 0)
|
||||
}
|
||||
|
||||
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
return ioutil.WriteFile(fs.path(name), contents, perm)
|
||||
}
|
||||
|
||||
@@ -5,31 +5,65 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
func TestSetDNS(t *testing.T) {
|
||||
const orig = "nameserver 9.9.9.9 # orig"
|
||||
func TestDirectManager(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
resolvPath := filepath.Join(tmp, "etc", "resolv.conf")
|
||||
backupPath := filepath.Join(tmp, "etc", "resolv.pre-tailscale-backup.conf")
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(resolvPath), 0777); err != nil {
|
||||
if err := os.MkdirAll(filepath.Join(tmp, "etc"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(resolvPath, []byte(orig), 0644); err != nil {
|
||||
testDirect(t, directFS{prefix: tmp})
|
||||
}
|
||||
|
||||
type boundResolvConfFS struct {
|
||||
directFS
|
||||
}
|
||||
|
||||
func (fs boundResolvConfFS) Rename(old, new string) error {
|
||||
if old == "/etc/resolv.conf" || new == "/etc/resolv.conf" {
|
||||
return errors.New("cannot move to/from /etc/resolv.conf")
|
||||
}
|
||||
return fs.directFS.Rename(old, new)
|
||||
}
|
||||
|
||||
func (fs boundResolvConfFS) Remove(name string) error {
|
||||
if name == "/etc/resolv.conf" {
|
||||
return errors.New("cannot remove /etc/resolv.conf")
|
||||
}
|
||||
return fs.directFS.Remove(name)
|
||||
}
|
||||
|
||||
func TestDirectBrokenRename(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(tmp, "etc"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testDirect(t, boundResolvConfFS{directFS{prefix: tmp}})
|
||||
}
|
||||
|
||||
func testDirect(t *testing.T, fs wholeFileFS) {
|
||||
const orig = "nameserver 9.9.9.9 # orig"
|
||||
resolvPath := "/etc/resolv.conf"
|
||||
backupPath := "/etc/resolv.pre-tailscale-backup.conf"
|
||||
|
||||
if err := fs.WriteFile(resolvPath, []byte(orig), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
readFile := func(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := ioutil.ReadFile(path)
|
||||
b, err := fs.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -39,12 +73,12 @@ func TestSetDNS(t *testing.T) {
|
||||
if got := readFile(t, resolvPath); got != orig {
|
||||
t.Fatalf("resolv.conf:\n%s, want:\n%s", got, orig)
|
||||
}
|
||||
if _, err := os.Stat(backupPath); !os.IsNotExist(err) {
|
||||
if _, err := fs.Stat(backupPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("resolv.conf backup: want it to be gone but: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
m := directManager{fs: directFS{prefix: tmp}}
|
||||
m := directManager{logf: t.Logf, fs: fs}
|
||||
if err := m.SetDNS(OSConfig{
|
||||
Nameservers: []netaddr.IP{netaddr.MustParseIP("8.8.8.8"), netaddr.MustParseIP("8.8.4.4")},
|
||||
SearchDomains: []dnsname.FQDN{"ts.net.", "ts-dns.test."},
|
||||
@@ -81,3 +115,26 @@ search ts.net ts-dns.test
|
||||
}
|
||||
assertBaseState(t)
|
||||
}
|
||||
|
||||
type brokenRemoveFS struct {
|
||||
directFS
|
||||
}
|
||||
|
||||
func (b brokenRemoveFS) Rename(old, new string) error {
|
||||
return errors.New("nyaaah I'm a silly container!")
|
||||
}
|
||||
|
||||
func (b brokenRemoveFS) Remove(name string) error {
|
||||
if strings.Contains(name, "/etc/resolv.conf") {
|
||||
return fmt.Errorf("Faking remove failure: %q", &fs.PathError{Err: syscall.EBUSY})
|
||||
}
|
||||
return b.directFS.Remove(name)
|
||||
}
|
||||
|
||||
func TestDirectBrokenRemove(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(tmp, "etc"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testDirect(t, brokenRemoveFS{directFS{prefix: tmp}})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
bs, err := ioutil.ReadFile("/etc/resolv.conf")
|
||||
if os.IsNotExist(err) {
|
||||
return newDirectManager(), nil
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
@@ -25,16 +25,16 @@ func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
case "resolvconf":
|
||||
switch resolvconfStyle() {
|
||||
case "":
|
||||
return newDirectManager(), nil
|
||||
return newDirectManager(logf), nil
|
||||
case "debian":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager()
|
||||
default:
|
||||
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle())
|
||||
return newDirectManager(), nil
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
default:
|
||||
return newDirectManager(), nil
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
}
|
||||
switch mode {
|
||||
case "direct":
|
||||
return newDirectManagerOnFS(env.fs), nil
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
case "systemd-resolved":
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
case "network-manager":
|
||||
@@ -51,7 +51,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
return newOpenresolvManager()
|
||||
default:
|
||||
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
|
||||
return newDirectManagerOnFS(env.fs), nil
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,20 +180,55 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
return "direct", nil
|
||||
}
|
||||
case "NetworkManager":
|
||||
// You'd think we would use newNMManager somewhere in
|
||||
// here. However, as explained in
|
||||
// https://github.com/tailscale/tailscale/issues/1699 , using
|
||||
// NetworkManager for DNS configuration carries with it the
|
||||
// cost of losing IPv6 configuration on the Tailscale network
|
||||
// interface. So, when we can avoid it, we bypass
|
||||
// NetworkManager by replacing resolv.conf directly.
|
||||
//
|
||||
// If you ever try to put NMManager back here, keep in mind
|
||||
// that versions >=1.26.6 will ignore DNS configuration
|
||||
// anyway, so you still need a fallback path that uses
|
||||
// directManager.
|
||||
dbg("rc", "nm")
|
||||
return "direct", nil
|
||||
// Sometimes, NetworkManager owns the configuration but points
|
||||
// it at systemd-resolved.
|
||||
if err := resolvedIsActuallyResolver(bs); err != nil {
|
||||
dbg("resolved", "not-in-use")
|
||||
// You'd think we would use newNMManager here. However, as
|
||||
// explained in
|
||||
// https://github.com/tailscale/tailscale/issues/1699 ,
|
||||
// using NetworkManager for DNS configuration carries with
|
||||
// it the cost of losing IPv6 configuration on the
|
||||
// Tailscale network interface. So, when we can avoid it,
|
||||
// we bypass NetworkManager by replacing resolv.conf
|
||||
// directly.
|
||||
//
|
||||
// If you ever try to put NMManager back here, keep in mind
|
||||
// that versions >=1.26.6 will ignore DNS configuration
|
||||
// anyway, so you still need a fallback path that uses
|
||||
// directManager.
|
||||
return "direct", nil
|
||||
}
|
||||
dbg("nm-resolved", "yes")
|
||||
|
||||
if err := env.dbusPing("org.freedesktop.resolve1", "/org/freedesktop/resolve1"); err != nil {
|
||||
dbg("resolved", "no")
|
||||
return "direct", nil
|
||||
}
|
||||
|
||||
// See large comment above for reasons we'd use NM rather than
|
||||
// resolved. systemd-resolved is actually in charge of DNS
|
||||
// configuration, but in some cases we might need to configure
|
||||
// it via NetworkManager. All the logic below is probing for
|
||||
// that case: is NetworkManager running? If so, is it one of
|
||||
// the versions that requires direct interaction with it?
|
||||
if err := env.dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
|
||||
dbg("nm", "no")
|
||||
return "systemd-resolved", nil
|
||||
}
|
||||
safe, err := env.nmVersionBetween("1.26.0", "1.26.5")
|
||||
if err != nil {
|
||||
// Failed to figure out NM's version, can't make a correct
|
||||
// decision.
|
||||
return "", fmt.Errorf("checking NetworkManager version: %v", err)
|
||||
}
|
||||
if safe {
|
||||
dbg("nm-safe", "yes")
|
||||
return "network-manager", nil
|
||||
}
|
||||
dbg("nm-safe", "no")
|
||||
return "systemd-resolved", nil
|
||||
default:
|
||||
dbg("rc", "unknown")
|
||||
return "direct", nil
|
||||
@@ -244,6 +279,13 @@ func nmIsUsingResolved() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolvedIsActuallyResolver reports whether the given resolv.conf
|
||||
// bytes describe a configuration where systemd-resolved (127.0.0.53)
|
||||
// is the only configured nameserver.
|
||||
//
|
||||
// Returns an error if the configuration is something other than
|
||||
// exclusively systemd-resolved, or nil if the config is only
|
||||
// systemd-resolved.
|
||||
func resolvedIsActuallyResolver(bs []byte) error {
|
||||
cfg, err := readResolv(bytes.NewBuffer(bs))
|
||||
if err != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf(
|
||||
"# Managed by NetworkManager",
|
||||
"nameserver 10.0.0.1")),
|
||||
wantLog: "dns: [rc=nm ret=direct]",
|
||||
wantLog: "dns: [rc=nm resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
{
|
||||
@@ -142,6 +142,82 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
wantLog: "dns: [rc=resolved nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// More than one user has had resolvconf write a config that points to
|
||||
// systemd-resolved. We're better off using systemd-resolved.
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/3026
|
||||
name: "allegedly_resolvconf_but_actually_systemd-resolved",
|
||||
env: env(resolvDotConf(
|
||||
"# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)",
|
||||
"# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN",
|
||||
"# 127.0.0.53 is the systemd-resolved stub resolver.",
|
||||
"# run \"systemd-resolve --status\" to see details about the actual nameservers.",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [rc=resolved nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// More than one user has had resolvconf write a config that points to
|
||||
// systemd-resolved. We're better off using systemd-resolved.
|
||||
// ...but what if systemd-resolved isn't running?
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/3026
|
||||
name: "allegedly_resolvconf_but_actually_systemd-resolved_but_not_really",
|
||||
env: env(resolvDotConf(
|
||||
"# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)",
|
||||
"# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN",
|
||||
"# 127.0.0.53 is the systemd-resolved stub resolver.",
|
||||
"# run \"systemd-resolve --status\" to see details about the actual nameservers.",
|
||||
"nameserver 127.0.0.53")),
|
||||
wantLog: "dns: [rc=resolved resolved=no ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/3304
|
||||
name: "networkmanager_but_pointing_at_systemd-resolved",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.32.12", true)),
|
||||
wantLog: "dns: [rc=nm nm-resolved=yes nm-safe=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/3304
|
||||
name: "networkmanager_but_pointing_at_systemd-resolved_but_no_resolved",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
nmRunning("1.32.12", true)),
|
||||
wantLog: "dns: [rc=nm nm-resolved=yes resolved=no ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/3304
|
||||
name: "networkmanager_but_pointing_at_systemd-resolved_and_safe_nm",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.26.3", true)),
|
||||
wantLog: "dns: [rc=nm nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
want: "network-manager",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/3304
|
||||
name: "networkmanager_but_pointing_at_systemd-resolved_and_no_networkmanager",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [rc=nm nm-resolved=yes nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -186,8 +262,20 @@ func (m memFS) ReadFile(name string) ([]byte, error) {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
func (fs memFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
fs[name] = string(contents)
|
||||
func (m memFS) Truncate(name string) error {
|
||||
v, ok := m[name]
|
||||
if !ok {
|
||||
return fs.ErrNotExist
|
||||
}
|
||||
if s, ok := v.(string); ok {
|
||||
m[name] = s[:0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m memFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
m[name] = string(contents)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@ package dns
|
||||
|
||||
import "tailscale.com/types/logger"
|
||||
|
||||
func NewOSConfigurator(logger.Logf, string) (OSConfigurator, error) {
|
||||
return newDirectManager(), nil
|
||||
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -68,7 +69,7 @@ func isResolvedActive() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
config, err := newDirectManager().readResolvConf()
|
||||
config, err := newDirectManager(logger.Discard).readResolvFile(resolvConf)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -171,12 +172,26 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
|
||||
m.ifidx, linkDomains,
|
||||
).Store()
|
||||
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
|
||||
// Issue 3188: older systemd-resolved had argument length limits.
|
||||
// Trim out the *.arpa. entries and try again.
|
||||
err = m.resolved.CallWithContext(
|
||||
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
|
||||
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
|
||||
).Store()
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDomains: %w", err)
|
||||
}
|
||||
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
return fmt.Errorf("setLinkDefaultRoute: %w", call.Err)
|
||||
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
|
||||
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
|
||||
// but otherwise it's working good
|
||||
m.logf("[v1] failed to set SetLinkDefaultRoute: %v", call.Err)
|
||||
} else {
|
||||
return fmt.Errorf("setLinkDefaultRoute: %w", call.Err)
|
||||
}
|
||||
}
|
||||
|
||||
// Some best-effort setting of things, but resolved should do the
|
||||
@@ -228,3 +243,16 @@ func (m *resolvedManager) Close() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// linkDomainsWithoutReverseDNS returns a copy of v without
|
||||
// *.arpa. entries.
|
||||
func linkDomainsWithoutReverseDNS(v []resolvedLinkDomain) (ret []resolvedLinkDomain) {
|
||||
for _, d := range v {
|
||||
if strings.HasSuffix(d.Domain, ".arpa.") {
|
||||
// Oh well. At least the rest will work.
|
||||
continue
|
||||
}
|
||||
ret = append(ret, d)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@ import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -179,19 +181,37 @@ func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *forwarder {
|
||||
maxDoHInFlight := 1000 // effectively unlimited
|
||||
if runtime.GOOS == "ios" {
|
||||
// No HTTP/2 on iOS yet (for size reasons), so DoH is
|
||||
// pricier.
|
||||
maxDoHInFlight = 10
|
||||
func maxDoHInFlight(goos string) int {
|
||||
if goos != "ios" {
|
||||
return 1000 // effectively unlimited
|
||||
}
|
||||
// iOS < 15 limits the memory to 15MB for NetworkExtensions.
|
||||
// iOS >= 15 gives us 50MB.
|
||||
// See: https://tailscale.com/blog/go-linker/
|
||||
ver := hostinfo.GetOSVersion()
|
||||
if ver == "" {
|
||||
// Unknown iOS version, be cautious.
|
||||
return 10
|
||||
}
|
||||
idx := strings.Index(ver, ".")
|
||||
if idx == -1 {
|
||||
// Unknown iOS version, be cautious.
|
||||
return 10
|
||||
}
|
||||
major := ver[:idx]
|
||||
if m, err := strconv.Atoi(major); err != nil || m < 15 {
|
||||
return 10
|
||||
}
|
||||
return 1000
|
||||
}
|
||||
|
||||
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *forwarder {
|
||||
f := &forwarder{
|
||||
logf: logger.WithPrefix(logf, "forward: "),
|
||||
linkMon: linkMon,
|
||||
linkSel: linkSel,
|
||||
responses: responses,
|
||||
dohSem: make(chan struct{}, maxDoHInFlight),
|
||||
dohSem: make(chan struct{}, maxDoHInFlight(runtime.GOOS)),
|
||||
}
|
||||
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
|
||||
return f
|
||||
@@ -322,7 +342,7 @@ func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Cl
|
||||
if f.dohClient == nil {
|
||||
f.dohClient = map[string]*http.Client{}
|
||||
}
|
||||
nsDialer := netns.NewDialer()
|
||||
nsDialer := netns.NewDialer(f.logf)
|
||||
c = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
IdleConnTimeout: dohTransportTimeout,
|
||||
@@ -538,7 +558,6 @@ func (f *forwarder) forward(query packet) error {
|
||||
// when browsing for LAN devices. But even when filtering this
|
||||
// out, playing on Sonos still works.
|
||||
if hasRDNSBonjourPrefix(domain) {
|
||||
f.logf("[v1] dropping %q", domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/types/dnstype"
|
||||
)
|
||||
|
||||
@@ -140,3 +141,30 @@ func TestGetRCode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxDoHInFlight(t *testing.T) {
|
||||
tests := []struct {
|
||||
goos string
|
||||
ver string
|
||||
want int
|
||||
}{
|
||||
{"ios", "", 10},
|
||||
{"ios", "1532", 10},
|
||||
{"ios", "9.3.2", 10},
|
||||
{"ios", "14.3.2", 10},
|
||||
{"ios", "15.3.2", 1000},
|
||||
{"ios", "20.3.2", 1000},
|
||||
{"android", "", 1000},
|
||||
{"darwin", "", 1000},
|
||||
{"linux", "", 1000},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(fmt.Sprintf("%s-%s", tc.goos, tc.ver), func(t *testing.T) {
|
||||
hostinfo.SetOSVersion(tc.ver)
|
||||
got := maxDoHInFlight(tc.goos)
|
||||
if got != tc.want {
|
||||
t.Errorf("got %d; want %d", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,7 +952,7 @@ func TestAllocs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query []byte
|
||||
want int
|
||||
want uint64
|
||||
}{
|
||||
// Name lowercasing, response slice created by dns.NewBuilder,
|
||||
// and closure allocation from go call.
|
||||
@@ -964,11 +964,11 @@ func TestAllocs(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
err := tstest.MinAllocsPerRun(t, tt.want, func() {
|
||||
syncRespond(r, tt.query)
|
||||
})
|
||||
if int(allocs) > tt.want {
|
||||
t.Errorf("%s: allocs = %v; want %v", tt.name, allocs, tt.want)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", tt.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +99,9 @@ func (wm *wslManager) SetDNS(cfg OSConfig) error {
|
||||
} else if len(distros) == 0 {
|
||||
return nil
|
||||
}
|
||||
managers := make(map[string]directManager)
|
||||
managers := make(map[string]*directManager)
|
||||
for _, distro := range distros {
|
||||
managers[distro] = newDirectManagerOnFS(wslFS{
|
||||
managers[distro] = newDirectManagerOnFS(wm.logf, wslFS{
|
||||
user: "root",
|
||||
distro: distro,
|
||||
})
|
||||
@@ -141,7 +141,7 @@ generateResolvConf = false
|
||||
|
||||
// setWSLConf attempts to disable generateResolvConf in each WSL2 linux.
|
||||
// If any are changed, it reports true.
|
||||
func (wm *wslManager) setWSLConf(managers map[string]directManager) (changed bool) {
|
||||
func (wm *wslManager) setWSLConf(managers map[string]*directManager) (changed bool) {
|
||||
for distro, m := range managers {
|
||||
b, err := m.fs.ReadFile(wslConf)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
@@ -189,6 +189,8 @@ func (fs wslFS) Rename(oldName, newName string) error {
|
||||
}
|
||||
func (fs wslFS) Remove(name string) error { return wslRun(fs.cmd("rm", "--", name)) }
|
||||
|
||||
func (fs wslFS) Truncate(name string) error { return fs.WriteFile(name, nil, 0644) }
|
||||
|
||||
func (fs wslFS) ReadFile(name string) ([]byte, error) {
|
||||
b, err := wslCombinedOutput(fs.cmd("cat", "--", name))
|
||||
if ee, _ := err.(*exec.ExitError); ee != nil && ee.ExitCode() == 1 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user