Compare commits
60 Commits
bradfitz/a
...
crawshaw/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5166565a7 | ||
|
|
516e8a4838 | ||
|
|
dd10babaed | ||
|
|
c7d4bf2333 | ||
|
|
2889fabaef | ||
|
|
761188e5d2 | ||
|
|
914a486af6 | ||
|
|
60e189f699 | ||
|
|
006a224f50 | ||
|
|
fe7c3e9c17 | ||
|
|
0bc73f8e4f | ||
|
|
c611d8480b | ||
|
|
c7fc4a06da | ||
|
|
de497358b8 | ||
|
|
1e28207a15 | ||
|
|
7a16ac80b7 | ||
|
|
4d943536f1 | ||
|
|
9f5b0d058f | ||
|
|
4dab0c1702 | ||
|
|
35e10c78fc | ||
|
|
692a011b54 | ||
|
|
e970ed0995 | ||
|
|
a7edcd0872 | ||
|
|
a98538f84a | ||
|
|
c3c59445ff | ||
|
|
0dde8fa0a8 | ||
|
|
4d3c09ced4 | ||
|
|
567c5a6d9e | ||
|
|
4fea604979 | ||
|
|
bf6205d200 | ||
|
|
9f7cbf6cf1 | ||
|
|
9ce92aad3e | ||
|
|
fa3543d629 | ||
|
|
e7bf144c3f | ||
|
|
97496a83af | ||
|
|
eb47cba435 | ||
|
|
daf2c70a08 | ||
|
|
d5baeeed5c | ||
|
|
4306433d1c | ||
|
|
9541886856 | ||
|
|
49d00b6a28 | ||
|
|
54d0d83b67 | ||
|
|
fec9490378 | ||
|
|
c55d26967b | ||
|
|
9f1b02699a | ||
|
|
a905ce5607 | ||
|
|
359055d3fa | ||
|
|
b5628cee4e | ||
|
|
edf64e0901 | ||
|
|
ec77b80c53 | ||
|
|
b5b4992eff | ||
|
|
d3dd7c6270 | ||
|
|
187e22a756 | ||
|
|
ab9cccb292 | ||
|
|
78338ac029 | ||
|
|
b405644f5d | ||
|
|
5fe5402fcd | ||
|
|
e4c075cd95 | ||
|
|
edce91a8a6 | ||
|
|
51bd1feae4 |
@@ -48,6 +48,9 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG goflags_arg # default intentionally unset
|
||||
ENV GOFLAGS=$goflags_arg
|
||||
|
||||
RUN go install -v ./cmd/...
|
||||
|
||||
FROM alpine:3.11
|
||||
|
||||
46
LICENSE
46
LICENSE
@@ -1,27 +1,29 @@
|
||||
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Tailscale Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.3.0
|
||||
1.5.0
|
||||
|
||||
31
build_docker.sh
Executable file
31
build_docker.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Runs `go build` with flags configured for docker distribution. All
|
||||
# it does differently from `go build` is burn git commit and version
|
||||
# information into the binaries inside docker, so that we can track down user
|
||||
# issues.
|
||||
#
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
|
||||
set -eu
|
||||
|
||||
eval $(./version/version.sh)
|
||||
|
||||
GOFLAGS='-tags xversion -ldflags '"-X tailscale.com/version.Long=${VERSION_LONG} -X tailscale.com/version.Short=${VERSION_SHORT} -X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}"
|
||||
docker build --build-arg goflags_arg="'""${GOFLAGS}""'" -t tailscale:tailscale .
|
||||
134
cmd/hello/hello.go
Normal file
134
cmd/hello/hello.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The hello binary runs hello.ipn.dev.
|
||||
package main // import "tailscale.com/cmd/hello"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
|
||||
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
http.HandleFunc("/", root)
|
||||
log.Printf("Starting hello server.")
|
||||
|
||||
errc := make(chan error, 1)
|
||||
if *httpAddr != "" {
|
||||
log.Printf("running HTTP server on %s", *httpAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServe(*httpAddr, nil)
|
||||
}()
|
||||
}
|
||||
if *httpsAddr != "" {
|
||||
log.Printf("running HTTPS server on %s", *httpsAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServeTLS(*httpsAddr,
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
nil,
|
||||
)
|
||||
}()
|
||||
}
|
||||
log.Fatal(<-errc)
|
||||
}
|
||||
|
||||
func slurpHTML() string {
|
||||
slurp, err := ioutil.ReadFile("hello.tmpl.html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return string(slurp)
|
||||
}
|
||||
|
||||
var tmpl = template.Must(template.New("home").Parse(slurpHTML()))
|
||||
|
||||
type tmplData struct {
|
||||
DisplayName string // "Foo Barberson"
|
||||
LoginName string // "foo@bar.com"
|
||||
MachineName string // "imac5k"
|
||||
IP string // "100.2.3.4"
|
||||
}
|
||||
|
||||
func root(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil && *httpsAddr != "" {
|
||||
host := r.Host
|
||||
if strings.Contains(r.Host, "100.101.102.103") {
|
||||
host = "hello.ipn.dev"
|
||||
}
|
||||
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if r.RequestURI != "/" {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
http.Error(w, "no remote addr", 500)
|
||||
return
|
||||
}
|
||||
who, err := whoIs(ip)
|
||||
if err != nil {
|
||||
log.Printf("whois(%q) error: %v", ip, err)
|
||||
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl.Execute(w, tmplData{
|
||||
DisplayName: who.UserProfile.DisplayName,
|
||||
LoginName: who.UserProfile.LoginName,
|
||||
MachineName: who.Node.ComputedName,
|
||||
IP: ip,
|
||||
})
|
||||
}
|
||||
|
||||
// tsSockClient does HTTP requests to the local Tailscale daemon.
|
||||
// The hostname in the HTTP request is ignored.
|
||||
var tsSockClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return safesocket.ConnectDefault()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func whoIs(ip string) (*tailcfg.WhoIsResponse, error) {
|
||||
res, err := tsSockClient.Get("http://local-tailscaled.sock/localapi/v0/whois?ip=" + url.QueryEscape(ip))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
slurp, _ := ioutil.ReadAll(res.Body)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
|
||||
}
|
||||
r := new(tailcfg.WhoIsResponse)
|
||||
if err := json.Unmarshal(slurp, r); err != nil {
|
||||
if max := 200; len(slurp) > max {
|
||||
slurp = slurp[:max]
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
17
cmd/hello/hello.tmpl.html
Normal file
17
cmd/hello/hello.tmpl.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello from Tailscale</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello!</h1>
|
||||
<p>
|
||||
Hello {{.DisplayName}} ({{.LoginName}}) from {{.MachineName}} ({{.IP}}).
|
||||
</p>
|
||||
<p>
|
||||
<b>Your Tailscale is working!</b>
|
||||
</p>
|
||||
<p>
|
||||
Welcome to Tailscale.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -64,34 +64,63 @@ func runPing(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 {
|
||||
if len(args) != 1 || args[0] == "" {
|
||||
return errors.New("usage: ping <hostname-or-IP>")
|
||||
}
|
||||
hostOrIP := args[0]
|
||||
var ip string
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
ch := make(chan *ipnstate.PingResult, 1)
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
stc := make(chan *ipnstate.Status, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
ch <- pr
|
||||
prc <- pr
|
||||
}
|
||||
if n.Status != nil {
|
||||
stc <- n.Status
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
hostOrIP := args[0]
|
||||
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
ip = hostOrIP
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
if ip == "" {
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case st := <-stc:
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
ip = ps.TailAddr
|
||||
break
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
if ip == "" {
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
n := 0
|
||||
anyPong := false
|
||||
for {
|
||||
@@ -101,7 +130,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case pr := <-ch:
|
||||
case pr := <-prc:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
return errors.New(pr.Err)
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -181,7 +180,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
|
||||
ipnstate.SortPeers(peers)
|
||||
for _, ps := range peers {
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
@@ -211,16 +210,6 @@ func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_"))
|
||||
}
|
||||
|
||||
func sortKey(ps *ipnstate.PeerStatus) string {
|
||||
if ps.DNSName != "" {
|
||||
return ps.DNSName
|
||||
}
|
||||
if ps.HostName != "" {
|
||||
return ps.HostName
|
||||
}
|
||||
return ps.TailAddr
|
||||
}
|
||||
|
||||
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
if ps.UserID.IsZero() {
|
||||
return "-"
|
||||
|
||||
@@ -120,6 +120,11 @@ func checkIPForwarding() {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
@@ -139,6 +144,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
@@ -149,8 +155,18 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routes = append(routes, ipp)
|
||||
}
|
||||
if default4 && !default6 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
|
||||
@@ -212,7 +228,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
fatalf("backend error: %v\n", *n.ErrMessage)
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
msg += " (try 'sudo tailscale up [...]')"
|
||||
}
|
||||
}
|
||||
fatalf("backend error: %v\n", msg)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
|
||||
@@ -27,7 +27,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
@@ -66,6 +65,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -88,6 +88,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
|
||||
@@ -31,7 +31,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
@@ -103,6 +102,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
@@ -112,7 +112,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/wgengine/tsdns+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
@@ -129,6 +129,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
|
||||
@@ -103,10 +103,6 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
if args.socketpath == "" && runtime.GOOS != "windows" {
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
@@ -140,6 +136,10 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
|
||||
@@ -20,22 +20,5 @@ CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
Type=notify
|
||||
|
||||
DeviceAllow=/dev/net/tun
|
||||
DeviceAllow=/dev/null
|
||||
DeviceAllow=/dev/random
|
||||
DeviceAllow=/dev/urandom
|
||||
DevicePolicy=strict
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
PrivateTmp=true
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/etc/
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -32,7 +32,9 @@ import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -96,7 +98,13 @@ func handleSSH(s ssh.Session) {
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !interfaces.IsTailscaleIP(ta.IP) {
|
||||
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
|
||||
if !ok {
|
||||
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(tanetaddr) {
|
||||
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
|
||||
@@ -744,6 +744,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
}
|
||||
resp.Peers = filtered
|
||||
}
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
// We need at least one endpoint here for now else
|
||||
// other code doesn't even create the discoEndpoint.
|
||||
// TODO(bradfitz): fix that and then just nil this out.
|
||||
p.Endpoints = []string{"127.9.9.9:456"}
|
||||
}
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||
@@ -762,6 +770,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
c.mu.Unlock()
|
||||
|
||||
nm := &NetworkMap{
|
||||
SelfNode: resp.Node,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
@@ -790,7 +799,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
}
|
||||
}
|
||||
addUserProfile(nm.User)
|
||||
magicDNSSuffix := nm.MagicDNSSuffix()
|
||||
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
|
||||
for _, peer := range resp.Peers {
|
||||
peer.InitDisplayNames(magicDNSSuffix)
|
||||
if !peer.Sharer.IsZero() {
|
||||
if c.keepSharerAndUserSplit {
|
||||
addUserProfile(peer.Sharer)
|
||||
@@ -972,19 +984,21 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
|
||||
var Debug = initDebug()
|
||||
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
use := os.Getenv("TS_DEBUG_USE_DISCO")
|
||||
return debug{
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1065,6 +1079,24 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
|
||||
if mapRes.PeerSeenChange != nil {
|
||||
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
||||
for _, n := range newFull {
|
||||
peerByID[n.ID] = n
|
||||
}
|
||||
now := time.Now()
|
||||
for nodeID, seen := range mapRes.PeerSeenChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
if seen {
|
||||
n.LastSeen = &now
|
||||
} else {
|
||||
n.LastSeen = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
|
||||
@@ -13,18 +13,18 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
type NetworkMap struct {
|
||||
// Core networking
|
||||
|
||||
SelfNode *tailcfg.Node
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgkey.Private
|
||||
Expiry time.Time
|
||||
@@ -63,27 +63,16 @@ type NetworkMap struct {
|
||||
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
||||
}
|
||||
|
||||
// MagicDNSSuffix returns the domain's MagicDNS suffix, or empty if none.
|
||||
// If non-empty, it will neither start nor end with a period.
|
||||
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if
|
||||
// MagicDNS isn't necessarily in use).
|
||||
//
|
||||
// It will neither start nor end with a period.
|
||||
func (nm *NetworkMap) MagicDNSSuffix() string {
|
||||
searchPathUsedAsDNSSuffix := func(suffix string) bool {
|
||||
if dnsname.HasSuffix(nm.Name, suffix) {
|
||||
return true
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if dnsname.HasSuffix(p.Name, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
name := strings.Trim(nm.Name, ".")
|
||||
if i := strings.Index(name, "."); i != -1 {
|
||||
name = name[i+1:]
|
||||
}
|
||||
|
||||
for _, d := range nm.DNS.Domains {
|
||||
if searchPathUsedAsDNSSuffix(d) {
|
||||
return strings.Trim(d, ".")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return name
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) String() string {
|
||||
@@ -312,12 +301,14 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Bits == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
logf("[v1] wgcfg: not accepting default route from %q (%v)",
|
||||
nodeDebugName(peer), peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)",
|
||||
allowedIP, nodeDebugName(peer), peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -328,6 +319,20 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func nodeDebugName(n *tailcfg.Node) string {
|
||||
name := n.Name
|
||||
if name == "" {
|
||||
name = n.Hostinfo.Hostname
|
||||
}
|
||||
if i := strings.Index(name, "."); i != -1 {
|
||||
name = name[:i]
|
||||
}
|
||||
if name == "" && len(n.Addresses) != 0 {
|
||||
return n.Addresses[0].String()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// cidrIsSubnet reports whether cidr is a non-default-route subnet
|
||||
// exported by node that is not one of its own self addresses.
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {
|
||||
|
||||
@@ -70,7 +70,7 @@ func Parse(p []byte) (Message, error) {
|
||||
case TypePong:
|
||||
return parsePong(ver, p)
|
||||
case TypeCallMeMaybe:
|
||||
return CallMeMaybe{}, nil
|
||||
return parseCallMeMaybe(ver, p)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
|
||||
}
|
||||
@@ -122,13 +122,57 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
|
||||
//
|
||||
// The recipient may choose to not open a path back, if it's already
|
||||
// happy with its path. But usually it will.
|
||||
type CallMeMaybe struct{}
|
||||
type CallMeMaybe struct {
|
||||
// MyNumber is what the peer believes its endpoints are.
|
||||
//
|
||||
// Prior to Tailscale 1.4, the endpoints were exchanged purely
|
||||
// between nodes and the control server.
|
||||
//
|
||||
// Starting with Tailscale 1.4, clients advertise their endpoints.
|
||||
// Older clients won't use this, but newer clients should
|
||||
// use any endpoints in here that aren't included from control.
|
||||
//
|
||||
// Control might have sent stale endpoints if the client was idle
|
||||
// before contacting us. In that case, the client likely did a STUN
|
||||
// request immediately before sending the CallMeMaybe to recreate
|
||||
// their NAT port mapping, and that new good endpoint is included
|
||||
// in this field, but might not yet be in control's endpoints.
|
||||
// (And in the future, control will stop distributing endpoints
|
||||
// when clients are suitably new.)
|
||||
MyNumber []netaddr.IPPort
|
||||
}
|
||||
|
||||
func (CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0)
|
||||
const epLength = 16 + 2 // 16 byte IP address + 2 byte port
|
||||
|
||||
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
|
||||
for _, ipp := range m.MyNumber {
|
||||
a := ipp.IP.As16()
|
||||
copy(p[:], a[:])
|
||||
binary.BigEndian.PutUint16(p[16:], ipp.Port)
|
||||
p = p[epLength:]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
|
||||
m = new(CallMeMaybe)
|
||||
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
m.MyNumber = make([]netaddr.IPPort, 0, len(p)/epLength)
|
||||
for len(p) > 0 {
|
||||
var a [16]byte
|
||||
copy(a[:], p)
|
||||
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
|
||||
IP: netaddr.IPFrom16(a),
|
||||
Port: binary.BigEndian.Uint16(p[16:18]),
|
||||
})
|
||||
p = p[epLength:]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Pong is a response a Ping.
|
||||
//
|
||||
// It includes the sender's source IP + port, so it's effectively a
|
||||
@@ -171,7 +215,7 @@ func MessageSummary(m Message) string {
|
||||
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
|
||||
case *Pong:
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case CallMeMaybe:
|
||||
case *CallMeMaybe:
|
||||
return "call-me-maybe"
|
||||
default:
|
||||
return fmt.Sprintf("%#v", m)
|
||||
|
||||
@@ -44,9 +44,19 @@ func TestMarshalAndParse(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe",
|
||||
m: CallMeMaybe{},
|
||||
m: &CallMeMaybe{},
|
||||
want: "03 00",
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe_endpoints",
|
||||
m: &CallMeMaybe{
|
||||
MyNumber: []netaddr.IPPort{
|
||||
netaddr.MustParseIPPort("1.2.3.4:567"),
|
||||
netaddr.MustParseIPPort("[2001::3456]:789"),
|
||||
},
|
||||
},
|
||||
want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -24,7 +24,7 @@ require (
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
|
||||
6
go.sum
6
go.sum
@@ -294,6 +294,12 @@ github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551 h1:hjBVxvVa
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4Ui+lzHm6gOq7s2Oe4ksxkbUYtS/JuoJ2Nce8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3 h1:wpgSErXul2ysBGZVVM0fKISMgZ9BZRXuOYAyn8MxAbY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8 h1:7OWHhbjWEuEjt+VlgOXLC4+iPkAvwTMU4zASxa+mKbw=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365 h1:0OC8+fnUCx5ww7uRSlzbcVC6Q/FK0PmVclmimbpWbyk=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestDeepPrint(t *testing.T) {
|
||||
|
||||
@@ -7,6 +7,7 @@ package ipnserver
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
"tailscale.com/util/systemd"
|
||||
@@ -113,10 +115,11 @@ type server struct {
|
||||
|
||||
// connIdentity represents the owner of a localhost TCP connection.
|
||||
type connIdentity struct {
|
||||
Unknown bool
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
Unknown bool
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
IsUnixSock bool
|
||||
}
|
||||
|
||||
// getConnIdentity returns the localhost TCP connection's identity information
|
||||
@@ -125,7 +128,9 @@ type connIdentity struct {
|
||||
// to be able to map it and couldn't.
|
||||
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
||||
return connIdentity{Unknown: true}, nil
|
||||
ci = connIdentity{Unknown: true}
|
||||
_, ci.IsUnixSock = c.(*net.UnixConn)
|
||||
return ci, nil
|
||||
}
|
||||
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
|
||||
if err != nil {
|
||||
@@ -503,41 +508,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
}()
|
||||
logf("Listening on %v", listen.Addr())
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
break
|
||||
}
|
||||
logf("ipnserver%d: getEngine failed again: %v", i, err)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var store ipn.StateStore
|
||||
if opts.StatePath != "" {
|
||||
store, err = ipn.NewFileStore(opts.StatePath)
|
||||
@@ -566,6 +536,82 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
store = &ipn.MemoryStore{}
|
||||
}
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
|
||||
// Issue 1187: on Windows, in unattended mode,
|
||||
// sometimes we try 5 times and fail to create the
|
||||
// engine before the system's ready. Hack until the
|
||||
// bug if fixed properly: if we're running in
|
||||
// unattended mode on Windows, keep trying forever,
|
||||
// waiting for the machine to be ready (networking to
|
||||
// come up?) and then dial our own safesocket TCP
|
||||
// listener to wake up the usual mechanism that lets
|
||||
// us surface getEngine errors to UI clients. (We
|
||||
// don't want to just call getEngine in a loop without
|
||||
// the listener.Accept, as we do want to handle client
|
||||
// connections so we can tell them about errors)
|
||||
|
||||
bootRaceWaitForEngine, bootRaceWaitForEngineCancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
if runtime.GOOS == "windows" && opts.AutostartStateKey != "" {
|
||||
logf("ipnserver: in unattended mode, waiting for engine availability")
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
// Wait for it to be ready.
|
||||
go func() {
|
||||
defer bootRaceWaitForEngineCancel()
|
||||
t0 := time.Now()
|
||||
for {
|
||||
time.Sleep(10 * time.Second)
|
||||
if _, err := getEngine(); err != nil {
|
||||
logf("ipnserver: unattended mode engine load: %v", err)
|
||||
continue
|
||||
}
|
||||
c, err := net.Dial("tcp", listen.Addr().String())
|
||||
logf("ipnserver: engine created after %v; waking up Accept: Dial error: %v", time.Since(t0).Round(time.Second), err)
|
||||
if err == nil {
|
||||
c.Close()
|
||||
}
|
||||
break
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
bootRaceWaitForEngineCancel()
|
||||
}
|
||||
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
<-bootRaceWaitForEngine.Done()
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
break
|
||||
}
|
||||
logf("ipnserver%d: getEngine failed again: %v", i, err)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
@@ -579,6 +625,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
|
||||
serveHTMLStatus(w, b)
|
||||
})
|
||||
opts.DebugMux.Handle("/localapi/v0/whois", whoIsHandler{b})
|
||||
}
|
||||
|
||||
server.b = b
|
||||
@@ -756,6 +803,27 @@ func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
|
||||
return func() (wgengine.Engine, error) { return eng, nil }
|
||||
}
|
||||
|
||||
// getEngineUntilItWorksWrapper returns a getEngine wrapper that does
|
||||
// not call getEngine concurrently and stops calling getEngine once
|
||||
// it's returned a working engine.
|
||||
func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, error)) func() (wgengine.Engine, error) {
|
||||
var mu sync.Mutex
|
||||
var engGood wgengine.Engine
|
||||
return func() (wgengine.Engine, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if engGood != nil {
|
||||
return engGood, nil
|
||||
}
|
||||
e, err := getEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engGood = e
|
||||
return e, nil
|
||||
}
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
@@ -798,6 +866,10 @@ func (psc *protoSwitchConn) Close() error {
|
||||
|
||||
func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ci.IsUnixSock && r.URL.Path == "/localapi/v0/whois" {
|
||||
whoIsHandler{s.b}.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if ci.Unknown {
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||
return
|
||||
@@ -821,3 +893,40 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// whoIsHandler is the debug server's /debug?ip=$IP HTTP handler.
|
||||
type whoIsHandler struct {
|
||||
b *ipn.LocalBackend
|
||||
}
|
||||
|
||||
func (h whoIsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
b := h.b
|
||||
var ip netaddr.IP
|
||||
if v := r.FormValue("ip"); v != "" {
|
||||
var err error
|
||||
ip, err = netaddr.ParseIP(r.FormValue("ip"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'ip' parameter", 400)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "missing 'ip' parameter", 400)
|
||||
return
|
||||
}
|
||||
n, u, ok := b.WhoIs(ip)
|
||||
if !ok {
|
||||
http.Error(w, "no match for IP", 404)
|
||||
return
|
||||
}
|
||||
res := &tailcfg.WhoIsResponse{
|
||||
Node: n,
|
||||
UserProfile: &u,
|
||||
}
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
@@ -21,14 +21,21 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// Status represents the entire state of the IPN network.
|
||||
type Status struct {
|
||||
BackendState string
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
MagicDNSSuffix string // e.g. "userfoo.tailscale.net" (no surrounding dots)
|
||||
BackendState string
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
// MagicDNSSuffix is the network's MagicDNS suffix for nodes
|
||||
// in the network such as "userfoo.tailscale.net".
|
||||
// There are no surrounding dots.
|
||||
// MagicDNSSuffix should be populated regardless of whether a domain
|
||||
// has MagicDNS enabled.
|
||||
MagicDNSSuffix string
|
||||
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
@@ -274,13 +281,22 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
|
||||
|
||||
f("<table>\n<thead>\n")
|
||||
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
|
||||
f("<tr><th>Peer</th><th>OS</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Connection</th></tr>\n")
|
||||
f("</thead>\n<tbody>\n")
|
||||
|
||||
now := time.Now()
|
||||
|
||||
var peers []*PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
SortPeers(peers)
|
||||
|
||||
for _, ps := range peers {
|
||||
var actAgo string
|
||||
if !ps.LastWrite.IsZero() {
|
||||
ago := now.Sub(ps.LastWrite)
|
||||
@@ -296,40 +312,44 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
owner = owner[:i]
|
||||
}
|
||||
}
|
||||
f("<tr><td>%s</td><td>%s %s<br><span class=\"tailaddr\">%s</span></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
|
||||
peer.ShortString(),
|
||||
html.EscapeString(ps.SimpleHostName()),
|
||||
|
||||
hostName := ps.SimpleHostName()
|
||||
dnsName := strings.TrimRight(ps.DNSName, ".")
|
||||
if i := strings.Index(dnsName, "."); i != -1 && dnsname.HasSuffix(dnsName, st.MagicDNSSuffix) {
|
||||
dnsName = dnsName[:i]
|
||||
}
|
||||
if strings.EqualFold(dnsName, hostName) || ps.UserID != st.Self.UserID {
|
||||
hostName = ""
|
||||
}
|
||||
var hostNameHTML string
|
||||
if hostName != "" {
|
||||
hostNameHTML = "<br>" + html.EscapeString(hostName)
|
||||
}
|
||||
|
||||
f("<tr><td>%s</td><td class=acenter>%s</td>"+
|
||||
"<td><b>%s</b>%s<div class=\"tailaddr\">%s</div></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
|
||||
ps.PublicKey.ShortString(),
|
||||
osEmoji(ps.OS),
|
||||
html.EscapeString(dnsName),
|
||||
hostNameHTML,
|
||||
ps.TailAddr,
|
||||
html.EscapeString(owner),
|
||||
ps.RxBytes,
|
||||
ps.TxBytes,
|
||||
actAgo,
|
||||
)
|
||||
f("<td class=\"aright\">")
|
||||
f("<td>")
|
||||
|
||||
// TODO: let server report this active bool instead
|
||||
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
relay := ps.Relay
|
||||
if relay != "" {
|
||||
if active && ps.CurAddr == "" {
|
||||
f("🔗 <b>derp-%v</b><br>", html.EscapeString(relay))
|
||||
} else {
|
||||
f("derp-%v<br>", html.EscapeString(relay))
|
||||
if active {
|
||||
if ps.Relay != "" && ps.CurAddr == "" {
|
||||
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct <b>%s</b>", html.EscapeString(ps.CurAddr))
|
||||
}
|
||||
}
|
||||
|
||||
match := false
|
||||
for _, addr := range ps.Addrs {
|
||||
if addr == ps.CurAddr {
|
||||
match = true
|
||||
f("🔗 <b>%s</b><br>", addr)
|
||||
} else {
|
||||
f("%s<br>", addr)
|
||||
}
|
||||
}
|
||||
if ps.CurAddr != "" && !match {
|
||||
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>", ps.CurAddr)
|
||||
}
|
||||
f("</td>") // end Addrs
|
||||
|
||||
f("</tr>\n")
|
||||
@@ -375,3 +395,17 @@ type PingResult struct {
|
||||
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
func SortPeers(peers []*PeerStatus) {
|
||||
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
|
||||
}
|
||||
|
||||
func sortKey(ps *PeerStatus) string {
|
||||
if ps.DNSName != "" {
|
||||
return ps.DNSName
|
||||
}
|
||||
if ps.HostName != "" {
|
||||
return ps.HostName
|
||||
}
|
||||
return ps.TailAddr
|
||||
}
|
||||
|
||||
56
ipn/local.go
56
ipn/local.go
@@ -15,7 +15,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
@@ -37,6 +36,7 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
var controlDebugFlags = getControlDebugFlags()
|
||||
@@ -90,6 +90,7 @@ type LocalBackend struct {
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
netMap *controlclient.NetworkMap
|
||||
nodeByAddr map[netaddr.IP]*tailcfg.Node
|
||||
activeLogin string // last logged LoginName from netMap
|
||||
engineStatus EngineStatus
|
||||
endpoints []string
|
||||
@@ -234,7 +235,22 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WhoIs reports the node and user who owns the node with the given IP.
|
||||
// If ok == true, n and u are valid.
|
||||
func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
n, ok = b.nodeByAddr[ip]
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
}
|
||||
u, ok = b.netMap.UserProfiles[n.User]
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
}
|
||||
return n, u, true
|
||||
}
|
||||
|
||||
// SetDecompressor sets a decompression function, which must be a zstd
|
||||
@@ -562,12 +578,13 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
|
||||
localNets := unmapIPPrefixes(netMap.Addresses, advRoutes)
|
||||
|
||||
oldFilter := b.e.GetFilter()
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(b.logf))
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, oldFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v", packetFilter)
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, oldFilter, b.logf))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1506,6 +1523,39 @@ func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
|
||||
b.logf("active login: %v", login)
|
||||
b.activeLogin = login
|
||||
}
|
||||
|
||||
if nm == nil {
|
||||
b.nodeByAddr = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Update the nodeByAddr index.
|
||||
if b.nodeByAddr == nil {
|
||||
b.nodeByAddr = map[netaddr.IP]*tailcfg.Node{}
|
||||
}
|
||||
// First pass, mark everything unwanted.
|
||||
for k := range b.nodeByAddr {
|
||||
b.nodeByAddr[k] = nil
|
||||
}
|
||||
addNode := func(n *tailcfg.Node) {
|
||||
for _, ipp := range n.Addresses {
|
||||
if ipp.IsSingleIP() {
|
||||
b.nodeByAddr[ipp.IP] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
if nm.SelfNode != nil {
|
||||
addNode(nm.SelfNode)
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
addNode(p)
|
||||
}
|
||||
// Third pass, actually delete the unwanted items.
|
||||
for k, v := range b.nodeByAddr {
|
||||
if v == nil {
|
||||
delete(b.nodeByAddr, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnlyPublicKeys returns the current machine and node public
|
||||
|
||||
@@ -146,6 +146,10 @@ func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
}
|
||||
|
||||
// ErrMsgPermissionDenied is the Notify.ErrMessage value used an
|
||||
// operation was done from a user/context that didn't have permission.
|
||||
const ErrMsgPermissionDenied = "permission denied"
|
||||
|
||||
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
|
||||
@@ -178,7 +182,7 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
}
|
||||
|
||||
if IsReadonlyContext(ctx) {
|
||||
msg := "permission denied"
|
||||
msg := ErrMsgPermissionDenied
|
||||
bs.send(Notify{ErrMessage: &msg})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -39,8 +40,11 @@ func Tailscale() (net.IP, *net.Interface, error) {
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok && IsTailscaleIP(ipnet.IP) {
|
||||
return ipnet.IP, &iface, nil
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netaddr.FromStdIP(ipnet.IP)
|
||||
if ok && tsaddr.IsTailscaleIP(nip) {
|
||||
return ipnet.IP, &iface, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,16 +61,21 @@ func maybeTailscaleInterfaceName(s string) bool {
|
||||
strings.HasPrefix(s, "utun")
|
||||
}
|
||||
|
||||
// IsTailscaleIP reports whether ip is an IP in a range used by
|
||||
// Tailscale virtual network interfaces.
|
||||
func IsTailscaleIP(ip net.IP) bool {
|
||||
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
|
||||
return tsaddr.IsTailscaleIP(nip)
|
||||
}
|
||||
|
||||
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
|
||||
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
|
||||
|
||||
func isProblematicInterface(nif *net.Interface) bool {
|
||||
name := nif.Name
|
||||
// Don't try to send disco/etc packets over zerotier; they effectively
|
||||
// DoS each other by doing traffic amplification, both of them
|
||||
// preferring/trying to use each other for transport. See:
|
||||
// https://github.com/tailscale/tailscale/issues/1208
|
||||
if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LocalAddresses returns the machine's IP addresses, separated by
|
||||
// whether they're loopback addresses.
|
||||
func LocalAddresses() (regular, loopback []string, err error) {
|
||||
@@ -77,8 +86,10 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
}
|
||||
for i := range ifaces {
|
||||
iface := &ifaces[i]
|
||||
if !isUp(iface) {
|
||||
// Down interfaces don't count
|
||||
if !isUp(iface) || isProblematicInterface(iface) {
|
||||
// Skip down interfaces and ones that are
|
||||
// problematic that we don't want to try to
|
||||
// send Tailscale traffic over.
|
||||
continue
|
||||
}
|
||||
ifcIsLoopback := isLoopback(iface)
|
||||
|
||||
@@ -15,7 +15,7 @@ package interfaces
|
||||
|
||||
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
|
||||
// Otherwise, it returns 0.
|
||||
int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
uint32_t privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
{
|
||||
// sockaddrs are after the message header
|
||||
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
|
||||
@@ -38,7 +38,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
return 0; // gateway not IPv4
|
||||
|
||||
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
|
||||
int ip;
|
||||
uint32_t ip;
|
||||
ip = gateway_si->sin_addr.s_addr;
|
||||
|
||||
unsigned char a, b;
|
||||
@@ -62,7 +62,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
// If no private gateway IP address was found, it returns 0.
|
||||
// On an error, it returns an error code in (0, 255].
|
||||
// Any private gateway IP address is > 255.
|
||||
int privateGatewayIP()
|
||||
uint32_t privateGatewayIP()
|
||||
{
|
||||
size_t needed;
|
||||
int mib[6];
|
||||
@@ -90,7 +90,7 @@ int privateGatewayIP()
|
||||
struct rt_msghdr2 *rtm;
|
||||
for (next = buf; next < lim; next += rtm->rtm_msglen) {
|
||||
rtm = (struct rt_msghdr2 *)next;
|
||||
int ip;
|
||||
uint32_t ip;
|
||||
ip = privateGatewayIPFromRoute(rtm);
|
||||
if (ip) {
|
||||
free(buf);
|
||||
|
||||
@@ -5,30 +5,9 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsTailscaleIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"100.81.251.94", true},
|
||||
{"8.8.8.8", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ip := net.ParseIP(tt.ip)
|
||||
if ip == nil {
|
||||
t.Fatalf("failed to parse IP %q", tt.ip)
|
||||
}
|
||||
got := IsTailscaleIP(ip)
|
||||
if got != tt.want {
|
||||
t.Errorf("F(%q) = %v; want %v", tt.ip, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetState(t *testing.T) {
|
||||
st, err := GetState()
|
||||
if err != nil {
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestBasics(t *testing.T) {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
fmt.Printf("server read %d bytes.\n", n)
|
||||
t.Logf("server read %d bytes.", n)
|
||||
if string(b[:n]) != "world" {
|
||||
errs <- fmt.Errorf("got %#v, expected %#v\n", string(b[:n]), "world")
|
||||
return
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type closeable interface {
|
||||
@@ -27,6 +29,11 @@ func ConnCloseWrite(c net.Conn) error {
|
||||
return c.(closeable).CloseWrite()
|
||||
}
|
||||
|
||||
// ConnectDefault connects to the local Tailscale daemon.
|
||||
func ConnectDefault() (net.Conn, error) {
|
||||
return Connect("/var/run/tailscale/tailscaled.sock", 41112)
|
||||
}
|
||||
|
||||
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
|
||||
func Connect(path string, port uint16) (net.Conn, error) {
|
||||
return connect(path, port)
|
||||
@@ -38,3 +45,21 @@ func Connect(path string, port uint16) (net.Conn, error) {
|
||||
func Listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) {
|
||||
return listen(path, port)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTokenNotFound = errors.New("no token found")
|
||||
ErrNoTokenOnOS = errors.New("no token on " + runtime.GOOS)
|
||||
)
|
||||
|
||||
var localTCPPortAndToken func() (port int, token string, err error)
|
||||
|
||||
// LocalTCPPortAndToken returns the port number and auth token to connect to
|
||||
// the local Tailscale daemon. It's currently only applicable on macOS
|
||||
// when tailscaled is being run in the Mac Sandbox from the App Store version
|
||||
// of Tailscale.
|
||||
func LocalTCPPortAndToken() (port int, token string, err error) {
|
||||
if localTCPPortAndToken == nil {
|
||||
return 0, "", ErrNoTokenOnOS
|
||||
}
|
||||
return localTCPPortAndToken()
|
||||
}
|
||||
|
||||
52
safesocket/safesocket_darwin.go
Normal file
52
safesocket/safesocket_darwin.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
localTCPPortAndToken = localTCPPortAndTokenDarwin
|
||||
}
|
||||
|
||||
func localTCPPortAndTokenDarwin() (port int, token string, err error) {
|
||||
out, err := exec.Command("lsof",
|
||||
"-n", // numeric sockets; don't do DNS lookups, etc
|
||||
"-a", // logical AND remaining options
|
||||
fmt.Sprintf("-u%d", os.Getuid()), // process of same user only
|
||||
"-c", "IPNExtension", // starting with IPNExtension
|
||||
"-F", // machine-readable output
|
||||
).Output()
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("failed to run lsof looking for IPNExtension: %w", err)
|
||||
}
|
||||
bs := bufio.NewScanner(bytes.NewReader(out))
|
||||
subStr := []byte(".tailscale.ipn.macos/sameuserproof-")
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
i := bytes.Index(line, subStr)
|
||||
if i == -1 {
|
||||
continue
|
||||
}
|
||||
f := strings.SplitN(string(line[i+len(subStr):]), "-", 2)
|
||||
if len(f) != 2 {
|
||||
continue
|
||||
}
|
||||
portStr, token := f[0], f[1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("invalid port %q found in lsof", portStr)
|
||||
}
|
||||
return port, token, nil
|
||||
}
|
||||
return 0, "", ErrTokenNotFound
|
||||
}
|
||||
13
safesocket/safesocket_test.go
Normal file
13
safesocket/safesocket_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package safesocket
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLocalTCPPortAndToken(t *testing.T) {
|
||||
// Just test that it compiles for now (is available on all platforms).
|
||||
port, token, err := LocalTCPPortAndToken()
|
||||
t.Logf("got %v, %s, %v", port, token, err)
|
||||
}
|
||||
@@ -7,17 +7,15 @@
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -59,12 +57,32 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
|
||||
return nil, 0, fmt.Errorf("%v: address already in use", path)
|
||||
}
|
||||
_ = os.Remove(path)
|
||||
os.MkdirAll(filepath.Dir(path), 0755) // best effort
|
||||
|
||||
perm := socketPermissionsForOS()
|
||||
|
||||
sockDir := filepath.Dir(path)
|
||||
if _, err := os.Stat(sockDir); os.IsNotExist(err) {
|
||||
os.MkdirAll(sockDir, 0755) // best effort
|
||||
|
||||
// If we're on a platform where we want the socket
|
||||
// world-readable, open up the permissions on the
|
||||
// just-created directory too, in case a umask ate
|
||||
// it. This primarily affects running tailscaled by
|
||||
// hand as root in a shell, as there is no umask when
|
||||
// running under systemd.
|
||||
if perm == 0666 {
|
||||
if fi, err := os.Stat(sockDir); err == nil && fi.Mode()&0077 == 0 {
|
||||
if err := os.Chmod(sockDir, 0755); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pipe, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
os.Chmod(path, socketPermissionsForOS())
|
||||
os.Chmod(path, perm)
|
||||
return pipe, 0, err
|
||||
}
|
||||
|
||||
@@ -146,42 +164,24 @@ func connectMacOSAppSandbox() (net.Conn, error) {
|
||||
}
|
||||
f := strings.SplitN(best.Name(), "-", 3)
|
||||
portStr, token := f[1], f[2]
|
||||
return connectMacTCP(portStr, token)
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid port %q", portStr)
|
||||
}
|
||||
return connectMacTCP(port, token)
|
||||
}
|
||||
|
||||
// Otherwise, assume we're running the cmd/tailscale binary from outside the
|
||||
// App Sandbox.
|
||||
|
||||
out, err := exec.Command("lsof",
|
||||
"-n", // numeric sockets; don't do DNS lookups, etc
|
||||
"-a", // logical AND remaining options
|
||||
fmt.Sprintf("-u%d", os.Getuid()), // process of same user only
|
||||
"-c", "IPNExtension", // starting with IPNExtension
|
||||
"-F", // machine-readable output
|
||||
).Output()
|
||||
port, token, err := LocalTCPPortAndToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bs := bufio.NewScanner(bytes.NewReader(out))
|
||||
subStr := []byte(".tailscale.ipn.macos/sameuserproof-")
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
i := bytes.Index(line, subStr)
|
||||
if i == -1 {
|
||||
continue
|
||||
}
|
||||
f := strings.SplitN(string(line[i+len(subStr):]), "-", 2)
|
||||
if len(f) != 2 {
|
||||
continue
|
||||
}
|
||||
portStr, token := f[0], f[1]
|
||||
return connectMacTCP(portStr, token)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find Tailscale's IPNExtension process")
|
||||
return connectMacTCP(port, token)
|
||||
}
|
||||
|
||||
func connectMacTCP(portStr, token string) (net.Conn, error) {
|
||||
c, err := net.Dial("tcp", "localhost:"+portStr)
|
||||
func connectMacTCP(port int, token string) (net.Conn, error) {
|
||||
c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error dialing IPNExtension: %w", err)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// CurrentMapRequestVersion is the current MapRequest.Version value.
|
||||
@@ -32,7 +33,10 @@ import (
|
||||
// 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
|
||||
// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side
|
||||
// 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains
|
||||
const CurrentMapRequestVersion = 9
|
||||
// 10: 2021-01-17: client understands MapResponse.PeerSeenChange
|
||||
const CurrentMapRequestVersion = 10
|
||||
|
||||
type StableID string
|
||||
|
||||
type ID int64
|
||||
|
||||
@@ -54,6 +58,12 @@ func (u NodeID) IsZero() bool {
|
||||
return u == 0
|
||||
}
|
||||
|
||||
type StableNodeID StableID
|
||||
|
||||
func (u StableNodeID) IsZero() bool {
|
||||
return u == ""
|
||||
}
|
||||
|
||||
type GroupID ID
|
||||
|
||||
func (u GroupID) IsZero() bool {
|
||||
@@ -147,8 +157,9 @@ type UserProfile struct {
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID NodeID
|
||||
Name string // DNS
|
||||
ID NodeID
|
||||
StableID StableNodeID
|
||||
Name string // DNS
|
||||
|
||||
// User is the user who created the node. If ACL tags are in
|
||||
// use for the node then it doesn't reflect the ACL identity
|
||||
@@ -173,6 +184,98 @@ type Node struct {
|
||||
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
|
||||
|
||||
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
|
||||
|
||||
// The following three computed fields hold the various names that can
|
||||
// be used for this node in UIs. They are populated from controlclient
|
||||
// (not from control) by calling node.InitDisplayNames. These can be
|
||||
// used directly or accessed via node.DisplayName or node.DisplayNames.
|
||||
|
||||
ComputedName string `json:",omitempty"` // MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
|
||||
computedHostIfDifferent string // hostname, if different than ComputedName, otherwise empty
|
||||
ComputedNameWithHost string `json:",omitempty"` // either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set
|
||||
}
|
||||
|
||||
// DisplayName returns the user-facing name for a node which should
|
||||
// be shown in client UIs.
|
||||
//
|
||||
// Parameter forOwner specifies whether the name is requested by
|
||||
// the owner of the node. When forOwner is false, the hostname is
|
||||
// never included in the return value.
|
||||
//
|
||||
// Return value is either either "Name" or "Name (Hostname)", where
|
||||
// Name is the node's MagicDNS base name (for normal non-shared-in
|
||||
// nodes), FQDN (without trailing dot, for shared-in nodes), or
|
||||
// Hostname (if no MagicDNS). Hostname is only included in the
|
||||
// return value if it varies from Name and forOwner is provided true.
|
||||
//
|
||||
// DisplayName is only valid if InitDisplayNames has been called.
|
||||
func (n *Node) DisplayName(forOwner bool) string {
|
||||
if forOwner {
|
||||
return n.ComputedNameWithHost
|
||||
}
|
||||
return n.ComputedName
|
||||
}
|
||||
|
||||
// DisplayName returns the decomposed user-facing name for a node.
|
||||
//
|
||||
// Parameter forOwner specifies whether the name is requested by
|
||||
// the owner of the node. When forOwner is false, hostIfDifferent
|
||||
// is always returned empty.
|
||||
//
|
||||
// Return value name is the node's primary name, populated with the
|
||||
// node's MagicDNS base name (for normal non-shared-in nodes), FQDN
|
||||
// (without trailing dot, for shared-in nodes), or Hostname (if no
|
||||
// MagicDNS).
|
||||
//
|
||||
// Return value hostIfDifferent, when non-empty, is the node's
|
||||
// hostname. hostIfDifferent is only populated when the hostname
|
||||
// varies from name and forOwner is provided as true.
|
||||
//
|
||||
// DisplayNames is only valid if InitDisplayNames has been called.
|
||||
func (n *Node) DisplayNames(forOwner bool) (name, hostIfDifferent string) {
|
||||
if forOwner {
|
||||
return n.ComputedName, n.computedHostIfDifferent
|
||||
}
|
||||
return n.ComputedName, ""
|
||||
}
|
||||
|
||||
// InitDisplayNames computes and populates n's display name
|
||||
// fields: n.ComputedName, n.computedHostIfDifferent, and
|
||||
// n.ComputedNameWithHost.
|
||||
func (n *Node) InitDisplayNames(networkMagicDNSSuffix string) {
|
||||
dnsName := n.Name
|
||||
if dnsName != "" {
|
||||
dnsName = strings.TrimRight(dnsName, ".")
|
||||
if i := strings.Index(dnsName, "."); i != -1 && dnsname.HasSuffix(dnsName, networkMagicDNSSuffix) {
|
||||
dnsName = dnsName[:i]
|
||||
}
|
||||
}
|
||||
|
||||
name := dnsName
|
||||
hostIfDifferent := n.Hostinfo.Hostname
|
||||
|
||||
if strings.EqualFold(name, hostIfDifferent) {
|
||||
hostIfDifferent = ""
|
||||
}
|
||||
if name == "" {
|
||||
if hostIfDifferent != "" {
|
||||
name = hostIfDifferent
|
||||
hostIfDifferent = ""
|
||||
} else {
|
||||
name = n.Key.String()
|
||||
}
|
||||
}
|
||||
|
||||
var nameWithHost string
|
||||
if hostIfDifferent != "" {
|
||||
nameWithHost = fmt.Sprintf("%s (%s)", name, hostIfDifferent)
|
||||
} else {
|
||||
nameWithHost = name
|
||||
}
|
||||
|
||||
n.ComputedName = name
|
||||
n.computedHostIfDifferent = hostIfDifferent
|
||||
n.ComputedNameWithHost = nameWithHost
|
||||
}
|
||||
|
||||
type MachineStatus int
|
||||
@@ -636,6 +739,11 @@ type MapResponse struct {
|
||||
// PeersRemoved are the NodeIDs that are no longer in the peer list.
|
||||
PeersRemoved []NodeID `json:",omitempty"`
|
||||
|
||||
// PeerSeenChange contains information on how to update peers' LastSeen
|
||||
// times. If the value is false, the peer is gone. If the value is true,
|
||||
// the LastSeen time is now. Absent means unchanged.
|
||||
PeerSeenChange map[NodeID]bool `json:",omitempty"`
|
||||
|
||||
// DNS is the same as DNSConfig.Nameservers.
|
||||
//
|
||||
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
|
||||
@@ -779,6 +887,7 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
}
|
||||
return n != nil && n2 != nil &&
|
||||
n.ID == n2.ID &&
|
||||
n.StableID == n2.StableID &&
|
||||
n.Name == n2.Name &&
|
||||
n.User == n2.User &&
|
||||
n.Sharer == n2.Sharer &&
|
||||
@@ -793,7 +902,10 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
n.Hostinfo.Equal(&n2.Hostinfo) &&
|
||||
n.Created.Equal(n2.Created) &&
|
||||
eqTimePtr(n.LastSeen, n2.LastSeen) &&
|
||||
n.MachineAuthorized == n2.MachineAuthorized
|
||||
n.MachineAuthorized == n2.MachineAuthorized &&
|
||||
n.ComputedName == n2.ComputedName &&
|
||||
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
|
||||
n.ComputedNameWithHost == n2.ComputedNameWithHost
|
||||
}
|
||||
|
||||
func eqStrings(a, b []string) bool {
|
||||
@@ -823,3 +935,9 @@ func eqCIDRs(a, b []netaddr.IPPrefix) bool {
|
||||
func eqTimePtr(a, b *time.Time) bool {
|
||||
return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
|
||||
}
|
||||
|
||||
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
|
||||
type WhoIsResponse struct {
|
||||
Node *Node
|
||||
UserProfile *UserProfile
|
||||
}
|
||||
|
||||
@@ -61,23 +61,27 @@ func (src *Node) Clone() *Node {
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
var _NodeNeedsRegeneration = Node(struct {
|
||||
ID NodeID
|
||||
Name string
|
||||
User UserID
|
||||
Sharer UserID
|
||||
Key NodeKey
|
||||
KeyExpiry time.Time
|
||||
Machine MachineKey
|
||||
DiscoKey DiscoKey
|
||||
Addresses []netaddr.IPPrefix
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints []string
|
||||
DERP string
|
||||
Hostinfo Hostinfo
|
||||
Created time.Time
|
||||
LastSeen *time.Time
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
ID NodeID
|
||||
StableID StableNodeID
|
||||
Name string
|
||||
User UserID
|
||||
Sharer UserID
|
||||
Key NodeKey
|
||||
KeyExpiry time.Time
|
||||
Machine MachineKey
|
||||
DiscoKey DiscoKey
|
||||
Addresses []netaddr.IPPrefix
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints []string
|
||||
DERP string
|
||||
Hostinfo Hostinfo
|
||||
Created time.Time
|
||||
LastSeen *time.Time
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
ComputedNameWithHost string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Hostinfo.
|
||||
|
||||
@@ -189,10 +189,11 @@ func TestHostinfoEqual(t *testing.T) {
|
||||
|
||||
func TestNodeEqual(t *testing.T) {
|
||||
nodeHandles := []string{
|
||||
"ID", "Name", "User", "Sharer",
|
||||
"ID", "StableID", "Name", "User", "Sharer",
|
||||
"Key", "KeyExpiry", "Machine", "DiscoKey",
|
||||
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
||||
"Created", "LastSeen", "KeepAlive", "MachineAuthorized",
|
||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
|
||||
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
@@ -229,6 +230,31 @@ func TestNodeEqual(t *testing.T) {
|
||||
&Node{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{},
|
||||
&Node{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{ID: 1},
|
||||
&Node{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{ID: 1},
|
||||
&Node{ID: 1},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{StableID: "node-abcd"},
|
||||
&Node{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{StableID: "node-abcd"},
|
||||
&Node{StableID: "node-abcd"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{User: 0},
|
||||
&Node{User: 1},
|
||||
|
||||
44
tstime/jitter.go
Normal file
44
tstime/jitter.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tstime
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// crandSource is a rand.Source64 that gets its numbers from
|
||||
// crypto/rand.Reader.
|
||||
type crandSource struct{ sync.Mutex }
|
||||
|
||||
var _ rand.Source64 = (*crandSource)(nil)
|
||||
|
||||
func (s *crandSource) Int63() int64 { return int64(s.Uint64() >> 1) }
|
||||
|
||||
func (s *crandSource) Uint64() uint64 {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
var buf [8]byte
|
||||
crand.Read(buf[:])
|
||||
return binary.BigEndian.Uint64(buf[:])
|
||||
}
|
||||
|
||||
func (*crandSource) Seed(seed int64) {} // nope
|
||||
|
||||
var durRand = rand.New(new(crandSource))
|
||||
|
||||
// RandomDurationBetween returns a random duration in range [min,max).
|
||||
// If panics if max < min.
|
||||
func RandomDurationBetween(min, max time.Duration) time.Duration {
|
||||
diff := max - min
|
||||
if diff == 0 {
|
||||
return min
|
||||
}
|
||||
ns := durRand.Int63n(int64(diff))
|
||||
return min + time.Duration(ns)
|
||||
}
|
||||
23
tstime/jitter_test.go
Normal file
23
tstime/jitter_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tstime
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRandomDurationBetween(t *testing.T) {
|
||||
if got := RandomDurationBetween(1, 1); got != 1 {
|
||||
t.Errorf("between 1 and 1 = %v; want 1", int64(got))
|
||||
}
|
||||
const min = 1 * time.Second
|
||||
const max = 10 * time.Second
|
||||
for i := 0; i < 500; i++ {
|
||||
if got := RandomDurationBetween(min, max); got < min || got >= max {
|
||||
t.Fatalf("%v (%d) out of range", got, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -41,6 +42,7 @@ func NewMux(debugHandler http.Handler) *http.ServeMux {
|
||||
|
||||
func registerCommonDebug(mux *http.ServeMux) {
|
||||
expvar.Publish("counter_uptime_sec", expvar.Func(func() interface{} { return int64(Uptime().Seconds()) }))
|
||||
expvar.Publish("gauge_goroutines", expvar.Func(func() interface{} { return runtime.NumGoroutine() }))
|
||||
mux.Handle("/debug/pprof/", Protected(http.DefaultServeMux)) // to net/http/pprof
|
||||
mux.Handle("/debug/vars", Protected(http.DefaultServeMux)) // to expvar
|
||||
mux.Handle("/debug/varz", Protected(http.HandlerFunc(VarzHandler)))
|
||||
@@ -81,8 +83,11 @@ func AllowDebugAccess(r *http.Request) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ip := net.ParseIP(ipStr)
|
||||
if interfaces.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("TS_ALLOW_DEBUG_IP") {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("TS_ALLOW_DEBUG_IP") {
|
||||
return true
|
||||
}
|
||||
if r.Method == "GET" {
|
||||
|
||||
@@ -64,9 +64,9 @@ type limitData struct {
|
||||
|
||||
var disableRateLimit = os.Getenv("TS_DEBUG_LOG_RATE") == "all"
|
||||
|
||||
// rateFreePrefix are format string prefixes that are exempt from rate limiting.
|
||||
// rateFree are format string substrings that are exempt from rate limiting.
|
||||
// Things should not be added to this unless they're already limited otherwise.
|
||||
var rateFreePrefix = []string{
|
||||
var rateFree = []string{
|
||||
"magicsock: disco: ",
|
||||
"magicsock: CreateEndpoint:",
|
||||
}
|
||||
@@ -93,8 +93,8 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
|
||||
)
|
||||
|
||||
judge := func(format string) verdict {
|
||||
for _, pfx := range rateFreePrefix {
|
||||
if strings.HasPrefix(format, pfx) {
|
||||
for _, sub := range rateFree {
|
||||
if strings.Contains(format, sub) {
|
||||
return allow
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
|
||||
logf(format, args...)
|
||||
case warn:
|
||||
// For the warning, log the specific format string
|
||||
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, fmt.Sprintf(format, args...))
|
||||
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, strings.TrimSpace(fmt.Sprintf(format, args...)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +132,16 @@ func NewAllowNone(logf logger.Logf) *Filter {
|
||||
return New(nil, nil, nil, logf)
|
||||
}
|
||||
|
||||
func NewShieldsUpFilter(logf logger.Logf) *Filter {
|
||||
f := New(nil, nil, nil, logf)
|
||||
// NewShieldsUpFilter returns a packet filter that rejects incoming connections.
|
||||
//
|
||||
// If shareStateWith is non-nil, the returned filter shares state with the previous one,
|
||||
// as long as the previous one was also a shields up filter.
|
||||
func NewShieldsUpFilter(localNets []netaddr.IPPrefix, shareStateWith *Filter, logf logger.Logf) *Filter {
|
||||
// Don't permit sharing state with a prior filter that wasn't a shields-up filter.
|
||||
if shareStateWith != nil && !shareStateWith.shieldsUp {
|
||||
shareStateWith = nil
|
||||
}
|
||||
f := New(nil, localNets, shareStateWith, logf)
|
||||
f.shieldsUp = true
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner --type=Match --output=match_clone.go
|
||||
|
||||
// PortRange is a range of TCP and UDP ports.
|
||||
type PortRange struct {
|
||||
First, Last uint16 // inclusive
|
||||
|
||||
31
wgengine/filter/match_clone.go
Normal file
31
wgengine/filter/match_clone.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Match; DO NOT EDIT.
|
||||
|
||||
package filter
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Match.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Match) Clone() *Match {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Match)
|
||||
*dst = *src
|
||||
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
|
||||
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Match
|
||||
var _MatchNeedsRegeneration = Match(struct {
|
||||
Dsts []NetPortRange
|
||||
Srcs []netaddr.IPPrefix
|
||||
}{})
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
"github.com/tailscale/wireguard-go/conn"
|
||||
"github.com/tailscale/wireguard-go/tai64n"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/poly1305"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -438,8 +438,17 @@ func (a *addrSet) DstToBytes() []byte {
|
||||
return packIPPort(a.dst())
|
||||
}
|
||||
func (a *addrSet) DstToString() string {
|
||||
dst := a.dst()
|
||||
return dst.String()
|
||||
var addrs []string
|
||||
for _, addr := range a.addrs {
|
||||
addrs = append(addrs, addr.String())
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.roamAddr != nil {
|
||||
addrs = append(addrs, a.roamAddr.String())
|
||||
}
|
||||
return strings.Join(addrs, ",")
|
||||
}
|
||||
func (a *addrSet) DstIP() net.IP {
|
||||
return a.dst().IP.IPAddr().IP // TODO: add netaddr accessor to cut an alloc here?
|
||||
@@ -578,20 +587,6 @@ func (as *addrSet) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *addrSet) Addrs() string {
|
||||
var addrs []string
|
||||
for _, addr := range a.addrs {
|
||||
addrs = append(addrs, addr.String())
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.roamAddr != nil {
|
||||
addrs = append(addrs, a.roamAddr.String())
|
||||
}
|
||||
return strings.Join(addrs, ",")
|
||||
}
|
||||
|
||||
// Message types copied from wireguard-go/device/noise-protocol.go
|
||||
const (
|
||||
messageInitiationType = 1
|
||||
|
||||
@@ -45,10 +45,10 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
@@ -161,6 +161,11 @@ type Conn struct {
|
||||
// whether a DERP channel read should be done.
|
||||
derpRecvCountLast int64 // owned by ReceiveIPv4
|
||||
|
||||
// ippEndpoint4 and ippEndpoint6 are owned by ReceiveIPv4 and
|
||||
// ReceiveIPv6, respectively, to cache an IPPort->endpoint for
|
||||
// hot flows.
|
||||
ippEndpoint4, ippEndpoint6 ippEndpointCache
|
||||
|
||||
// ============================================================
|
||||
mu sync.Mutex // guards all following fields; see userspaceEngine lock ordering rules
|
||||
muCond *sync.Cond
|
||||
@@ -168,6 +173,19 @@ type Conn struct {
|
||||
started bool // Start was called
|
||||
closed bool // Close was called
|
||||
|
||||
// derpCleanupTimer is the timer that fires to occasionally clean
|
||||
// up idle DERP connections. It's only used when there is a non-home
|
||||
// DERP connection in use.
|
||||
derpCleanupTimer *time.Timer
|
||||
|
||||
// derpCleanupTimerArmed is whether derpCleanupTimer is
|
||||
// scheduled to fire within derpCleanStaleInterval.
|
||||
derpCleanupTimerArmed bool
|
||||
|
||||
// periodicReSTUNTimer, when non-nil, is an AfterFunc timer
|
||||
// that will call Conn.doPeriodicSTUN.
|
||||
periodicReSTUNTimer *time.Timer
|
||||
|
||||
// endpointsUpdateActive indicates that updateEndpoints is
|
||||
// currently running. It's used to deduplicate concurrent endpoint
|
||||
// update requests.
|
||||
@@ -182,6 +200,14 @@ type Conn struct {
|
||||
// change notifications.
|
||||
lastEndpoints []string
|
||||
|
||||
// lastEndpointsTime is the last time the endpoints were updated,
|
||||
// even if there was no change.
|
||||
lastEndpointsTime time.Time
|
||||
|
||||
// onEndpointRefreshed are funcs to run (in their own goroutines)
|
||||
// when endpoints are refreshed.
|
||||
onEndpointRefreshed map[*discoEndpoint]func()
|
||||
|
||||
// peerSet is the set of peers that are currently configured in
|
||||
// WireGuard. These are not used to filter inbound or outbound
|
||||
// traffic at all, but only to track what state can be cleaned up
|
||||
@@ -477,13 +503,6 @@ func (c *Conn) Start() {
|
||||
c.mu.Unlock()
|
||||
|
||||
c.ReSTUN("initial")
|
||||
|
||||
// We assume that LinkChange notifications are plumbed through well
|
||||
// on our mobile clients, so don't do the timer thing to save radio/battery/CPU/etc.
|
||||
if !version.IsMobile() {
|
||||
go c.periodicReSTUN()
|
||||
}
|
||||
go c.periodicDerpCleanup()
|
||||
}
|
||||
|
||||
// ignoreSTUNPackets sets a STUN packet processing func that does nothing.
|
||||
@@ -491,6 +510,17 @@ func (c *Conn) ignoreSTUNPackets() {
|
||||
c.stunReceiveFunc.Store(func([]byte, netaddr.IPPort) {})
|
||||
}
|
||||
|
||||
// doPeriodicSTUN is called (in a new goroutine) by
|
||||
// periodicReSTUNTimer when periodic STUNs are active.
|
||||
func (c *Conn) doPeriodicSTUN() { c.ReSTUN("periodic") }
|
||||
|
||||
func (c *Conn) stopPeriodicReSTUNTimerLocked() {
|
||||
if t := c.periodicReSTUNTimer; t != nil {
|
||||
t.Stop()
|
||||
c.periodicReSTUNTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
// c.mu must NOT be held.
|
||||
func (c *Conn) updateEndpoints(why string) {
|
||||
defer func() {
|
||||
@@ -498,13 +528,37 @@ func (c *Conn) updateEndpoints(why string) {
|
||||
defer c.mu.Unlock()
|
||||
why := c.wantEndpointsUpdate
|
||||
c.wantEndpointsUpdate = ""
|
||||
if why != "" && !c.closed {
|
||||
go c.updateEndpoints(why)
|
||||
} else {
|
||||
c.endpointsUpdateActive = false
|
||||
c.muCond.Broadcast()
|
||||
if !c.closed {
|
||||
if why != "" {
|
||||
go c.updateEndpoints(why)
|
||||
return
|
||||
}
|
||||
if c.shouldDoPeriodicReSTUNLocked() {
|
||||
// Pick a random duration between 20
|
||||
// and 26 seconds (just under 30s, a
|
||||
// common UDP NAT timeout on Linux,
|
||||
// etc)
|
||||
d := tstime.RandomDurationBetween(20*time.Second, 26*time.Second)
|
||||
if t := c.periodicReSTUNTimer; t != nil {
|
||||
if debugReSTUNStopOnIdle {
|
||||
c.logf("resetting existing periodicSTUN to run in %v", d)
|
||||
}
|
||||
t.Reset(d)
|
||||
} else {
|
||||
if debugReSTUNStopOnIdle {
|
||||
c.logf("scheduling periodicSTUN to run in %v", d)
|
||||
}
|
||||
c.periodicReSTUNTimer = time.AfterFunc(d, c.doPeriodicSTUN)
|
||||
}
|
||||
} else {
|
||||
if debugReSTUNStopOnIdle {
|
||||
c.logf("periodic STUN idle")
|
||||
}
|
||||
c.stopPeriodicReSTUNTimerLocked()
|
||||
}
|
||||
}
|
||||
|
||||
c.endpointsUpdateActive = false
|
||||
c.muCond.Broadcast()
|
||||
}()
|
||||
c.logf("[v1] magicsock: starting endpoint update (%s)", why)
|
||||
|
||||
@@ -549,6 +603,12 @@ func (c *Conn) setEndpoints(endpoints []string, reasons map[string]string) (chan
|
||||
return false
|
||||
}
|
||||
|
||||
c.lastEndpointsTime = time.Now()
|
||||
for de, fn := range c.onEndpointRefreshed {
|
||||
go fn()
|
||||
delete(c.onEndpointRefreshed, de)
|
||||
}
|
||||
|
||||
if stringsEqual(endpoints, c.lastEndpoints) {
|
||||
return false
|
||||
}
|
||||
@@ -1214,6 +1274,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
|
||||
c.activeDerp[regionID] = ad
|
||||
c.logActiveDerpLocked()
|
||||
c.setPeerLastDerpLocked(peer, regionID, regionID)
|
||||
c.scheduleCleanStaleDerpLocked()
|
||||
|
||||
// Build a startGate for the derp reader+writer
|
||||
// goroutines, so they don't start running until any
|
||||
@@ -1483,7 +1544,7 @@ func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, error) {
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr)); ok {
|
||||
if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr), &c.ippEndpoint6); ok {
|
||||
return n, ep, nil
|
||||
}
|
||||
}
|
||||
@@ -1520,28 +1581,37 @@ func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, err error) {
|
||||
}
|
||||
return 0, nil, err
|
||||
}
|
||||
if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr)); ok {
|
||||
if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr), &c.ippEndpoint4); ok {
|
||||
return n, ep, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// receiveIP is the shared bits of ReceiveIPv4 and ReceiveIPv6.
|
||||
func (c *Conn) receiveIP(b []byte, ua *net.UDPAddr) (ep conn.Endpoint, ok bool) {
|
||||
func (c *Conn) receiveIP(b []byte, ua *net.UDPAddr, cache *ippEndpointCache) (ep conn.Endpoint, ok bool) {
|
||||
ipp, ok := netaddr.FromStdAddr(ua.IP, ua.Port, ua.Zone)
|
||||
if !ok {
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
if stun.Is(b) {
|
||||
c.stunReceiveFunc.Load().(func([]byte, netaddr.IPPort))(b, ipp)
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
if c.handleDiscoMessage(b, ipp) {
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
ep = c.findEndpoint(ipp, ua, b)
|
||||
if ep == nil {
|
||||
return
|
||||
if cache.ipp == ipp && cache.de != nil && cache.gen == cache.de.numStopAndReset() {
|
||||
ep = cache.de
|
||||
} else {
|
||||
ep = c.findEndpoint(ipp, ua, b)
|
||||
if ep == nil {
|
||||
return nil, false
|
||||
}
|
||||
if de, ok := ep.(*discoEndpoint); ok {
|
||||
cache.ipp = ipp
|
||||
cache.de = de
|
||||
cache.gen = de.numStopAndReset()
|
||||
}
|
||||
}
|
||||
c.noteRecvActivityFromEndpoint(ep)
|
||||
return ep, true
|
||||
@@ -1608,7 +1678,7 @@ func (c *Conn) receiveIPv4DERP(b []byte) (n int, ep conn.Endpoint, err error) {
|
||||
c.mu.Lock()
|
||||
|
||||
discoEp = c.endpointOfDisco[dk]
|
||||
c.logf("magicsock: DERP packet received from idle peer %v; created=%v", dm.src.ShortString(), ep != nil)
|
||||
c.logf("magicsock: DERP packet received from idle peer %v; created=%v", dm.src.ShortString(), discoEp != nil)
|
||||
}
|
||||
}
|
||||
if !c.disableLegacy {
|
||||
@@ -1820,15 +1890,18 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
|
||||
return true
|
||||
}
|
||||
de.handlePongConnLocked(dm, src)
|
||||
case disco.CallMeMaybe:
|
||||
case *disco.CallMeMaybe:
|
||||
if src.IP != derpMagicIPAddr {
|
||||
// CallMeMaybe messages should only come via DERP.
|
||||
c.logf("[unexpected] CallMeMaybe packets should only come via DERP")
|
||||
return true
|
||||
}
|
||||
if de != nil {
|
||||
c.logf("magicsock: disco: %v<-%v (%v, %v) got call-me-maybe", c.discoShort, de.discoShort, de.publicKey.ShortString(), derpStr(src.String()))
|
||||
go de.handleCallMeMaybe()
|
||||
c.logf("magicsock: disco: %v<-%v (%v, %v) got call-me-maybe, %d endpoints",
|
||||
c.discoShort, de.discoShort,
|
||||
de.publicKey.ShortString(), derpStr(src.String()),
|
||||
len(dm.MyNumber))
|
||||
go de.handleCallMeMaybe(dm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1859,6 +1932,47 @@ func (c *Conn) handlePingLocked(dm *disco.Ping, de *discoEndpoint, src netaddr.I
|
||||
}, discoVerboseLog)
|
||||
}
|
||||
|
||||
// enqueueCallMeMaybe schedules a send of disco.CallMeMaybe to de via derpAddr
|
||||
// once we know that our STUN endpoint is fresh.
|
||||
//
|
||||
// derpAddr is de.derpAddr at the time of send. It's assumed the peer won't be
|
||||
// flipping primary DERPs in the 0-30ms it takes to confirm our STUN endpoint.
|
||||
// If they do, traffic will just go over DERP for a bit longer until the next
|
||||
// discovery round.
|
||||
func (c *Conn) enqueueCallMeMaybe(derpAddr netaddr.IPPort, de *discoEndpoint) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if !c.lastEndpointsTime.After(time.Now().Add(-endpointsFreshEnoughDuration)) {
|
||||
c.logf("magicsock: want call-me-maybe but endpoints stale; restunning")
|
||||
if c.onEndpointRefreshed == nil {
|
||||
c.onEndpointRefreshed = map[*discoEndpoint]func(){}
|
||||
}
|
||||
c.onEndpointRefreshed[de] = func() {
|
||||
c.logf("magicsock: STUN done; sending call-me-maybe to %v %v", de.discoShort, de.publicKey.ShortString())
|
||||
c.enqueueCallMeMaybe(derpAddr, de)
|
||||
}
|
||||
// TODO(bradfitz): make a new 'reSTUNQuickly' method
|
||||
// that passes down a do-a-lite-netcheck flag down to
|
||||
// netcheck that does 1 (or 2 max) STUN queries
|
||||
// (UDP-only, not HTTPs) to find our port mapping to
|
||||
// our home DERP and maybe one other. For now we do a
|
||||
// "full" ReSTUN which may or may not be a full one
|
||||
// (depending on age) and may do HTTPS timing queries
|
||||
// (if UDP is blocked). Good enough for now.
|
||||
go c.ReSTUN("refresh-for-peering")
|
||||
return
|
||||
}
|
||||
|
||||
eps := make([]netaddr.IPPort, 0, len(c.lastEndpoints))
|
||||
for _, ep := range c.lastEndpoints {
|
||||
if ipp, err := netaddr.ParseIPPort(ep); err == nil {
|
||||
eps = append(eps, ipp)
|
||||
}
|
||||
}
|
||||
go de.sendDiscoMessage(derpAddr, &disco.CallMeMaybe{MyNumber: eps}, discoLog)
|
||||
}
|
||||
|
||||
// setAddrToDiscoLocked records that newk is at src.
|
||||
//
|
||||
// c.mu must be held.
|
||||
@@ -1978,6 +2092,8 @@ func (c *Conn) SetPrivateKey(privateKey wgkey.Private) error {
|
||||
} else if newKey.IsZero() {
|
||||
c.logf("magicsock: SetPrivateKey called (zeroed)")
|
||||
c.closeAllDerpLocked("zero-private-key")
|
||||
c.stopPeriodicReSTUNTimerLocked()
|
||||
c.onEndpointRefreshed = nil
|
||||
} else {
|
||||
c.logf("magicsock: SetPrivateKey called (changed)")
|
||||
c.closeAllDerpLocked("new-private-key")
|
||||
@@ -2185,9 +2301,14 @@ func (c *Conn) foreachActiveDerpSortedLocked(fn func(regionID int, ad activeDerp
|
||||
func (c *Conn) cleanStaleDerp() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
const inactivityTime = 60 * time.Second
|
||||
tooOld := time.Now().Add(-inactivityTime)
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
c.derpCleanupTimerArmed = false
|
||||
|
||||
tooOld := time.Now().Add(-derpInactiveCleanupTime)
|
||||
dirty := false
|
||||
someNonHomeOpen := false
|
||||
for i, ad := range c.activeDerp {
|
||||
if i == c.myDerp {
|
||||
continue
|
||||
@@ -2195,11 +2316,31 @@ func (c *Conn) cleanStaleDerp() {
|
||||
if ad.lastWrite.Before(tooOld) {
|
||||
c.closeDerpLocked(i, "idle")
|
||||
dirty = true
|
||||
} else {
|
||||
someNonHomeOpen = true
|
||||
}
|
||||
}
|
||||
if dirty {
|
||||
c.logActiveDerpLocked()
|
||||
}
|
||||
if someNonHomeOpen {
|
||||
c.scheduleCleanStaleDerpLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) scheduleCleanStaleDerpLocked() {
|
||||
if c.derpCleanupTimerArmed {
|
||||
// Already going to fire soon. Let the existing one
|
||||
// fire lest it get infinitely delayed by repeated
|
||||
// calls to scheduleCleanStaleDerpLocked.
|
||||
return
|
||||
}
|
||||
c.derpCleanupTimerArmed = true
|
||||
if c.derpCleanupTimer != nil {
|
||||
c.derpCleanupTimer.Reset(derpCleanStaleInterval)
|
||||
} else {
|
||||
c.derpCleanupTimer = time.AfterFunc(derpCleanStaleInterval, c.cleanStaleDerp)
|
||||
}
|
||||
}
|
||||
|
||||
// DERPs reports the number of active DERP connections.
|
||||
@@ -2222,6 +2363,10 @@ func (c *Conn) Close() error {
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
if c.derpCleanupTimerArmed {
|
||||
c.derpCleanupTimer.Stop()
|
||||
}
|
||||
c.stopPeriodicReSTUNTimerLocked()
|
||||
|
||||
for _, ep := range c.endpointOfDisco {
|
||||
ep.stopAndReset()
|
||||
@@ -2245,13 +2390,6 @@ func (c *Conn) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// isClosed reports whether c is closed.
|
||||
func (c *Conn) isClosed() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.closed
|
||||
}
|
||||
|
||||
func (c *Conn) goroutinesRunningLocked() bool {
|
||||
if c.endpointsUpdateActive {
|
||||
return true
|
||||
@@ -2277,86 +2415,36 @@ func (c *Conn) goroutinesRunningLocked() bool {
|
||||
|
||||
func maxIdleBeforeSTUNShutdown() time.Duration {
|
||||
if debugReSTUNStopOnIdle {
|
||||
return time.Minute
|
||||
return 45 * time.Second
|
||||
}
|
||||
return 5 * time.Minute
|
||||
return sessionActiveTimeout
|
||||
}
|
||||
|
||||
func (c *Conn) shouldDoPeriodicReSTUN() bool {
|
||||
func (c *Conn) shouldDoPeriodicReSTUNLocked() bool {
|
||||
if c.networkDown() {
|
||||
return false
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.peerSet) == 0 {
|
||||
// No peers, so not worth doing.
|
||||
if len(c.peerSet) == 0 || c.privateKey.IsZero() {
|
||||
// If no peers, not worth doing.
|
||||
// Also don't if there's no key (not running).
|
||||
return false
|
||||
}
|
||||
// If it turns out this optimization was a mistake, we can
|
||||
// override it from the control server without waiting for a
|
||||
// new software rollout:
|
||||
if c.netMap != nil && c.netMap.Debug != nil && c.netMap.Debug.ForceBackgroundSTUN && !debugReSTUNStopOnIdle {
|
||||
return true
|
||||
}
|
||||
if f := c.idleFunc; f != nil {
|
||||
idleFor := f()
|
||||
if debugReSTUNStopOnIdle {
|
||||
c.logf("magicsock: periodicReSTUN: idle for %v", idleFor.Round(time.Second))
|
||||
}
|
||||
if idleFor > maxIdleBeforeSTUNShutdown() {
|
||||
if debugReSTUNStopOnIdle || version.IsMobile() { // TODO: make this unconditional later
|
||||
return false
|
||||
if c.netMap != nil && c.netMap.Debug != nil && c.netMap.Debug.ForceBackgroundSTUN {
|
||||
// Overridden by control.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Conn) periodicReSTUN() {
|
||||
prand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
dur := func() time.Duration {
|
||||
// Just under 30s, a common UDP NAT timeout (Linux at least)
|
||||
return time.Duration(20+prand.Intn(7)) * time.Second
|
||||
}
|
||||
timer := time.NewTimer(dur())
|
||||
defer timer.Stop()
|
||||
var lastIdleState opt.Bool
|
||||
for {
|
||||
select {
|
||||
case <-c.donec:
|
||||
return
|
||||
case <-timer.C:
|
||||
doReSTUN := c.shouldDoPeriodicReSTUN()
|
||||
if !lastIdleState.EqualBool(doReSTUN) {
|
||||
if doReSTUN {
|
||||
c.logf("[v1] magicsock: periodicReSTUN enabled")
|
||||
} else {
|
||||
c.logf("[v1] magicsock: periodicReSTUN disabled due to inactivity")
|
||||
}
|
||||
lastIdleState.Set(doReSTUN)
|
||||
}
|
||||
if doReSTUN {
|
||||
c.ReSTUN("periodic")
|
||||
}
|
||||
timer.Reset(dur())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) periodicDerpCleanup() {
|
||||
ticker := time.NewTicker(15 * time.Second) // arbitrary
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.donec:
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.cleanStaleDerp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReSTUN triggers an address discovery.
|
||||
// The provided why string is for debug logging only.
|
||||
func (c *Conn) ReSTUN(why string) {
|
||||
@@ -2781,7 +2869,8 @@ func udpAddrDebugString(ua net.UDPAddr) string {
|
||||
// advertise a DiscoKey and participate in active discovery.
|
||||
type discoEndpoint struct {
|
||||
// atomically accessed; declared first for alignment reasons
|
||||
lastRecvUnixAtomic int64
|
||||
lastRecvUnixAtomic int64
|
||||
numStopAndResetAtomic int64
|
||||
|
||||
// These fields are initialized once and never modified.
|
||||
c *Conn
|
||||
@@ -2809,6 +2898,7 @@ type discoEndpoint struct {
|
||||
trustBestAddrUntil time.Time // time when bestAddr expires
|
||||
sentPing map[stun.TxID]sentPing
|
||||
endpointState map[netaddr.IPPort]*endpointState
|
||||
isCallMeMaybeEP map[netaddr.IPPort]bool
|
||||
|
||||
pendingCLIPings []pendingCLIPing // any outstanding "tailscale ping" commands running
|
||||
}
|
||||
@@ -2821,6 +2911,8 @@ type pendingCLIPing struct {
|
||||
const (
|
||||
// sessionActiveTimeout is how long since the last activity we
|
||||
// try to keep an established discoEndpoint peering alive.
|
||||
// It's also the idle time at which we stop doing STUN queries to
|
||||
// keep NAT mappings alive.
|
||||
sessionActiveTimeout = 2 * time.Minute
|
||||
|
||||
// upgradeInterval is how often we try to upgrade to a better path
|
||||
@@ -2848,6 +2940,19 @@ const (
|
||||
// goodEnoughLatency is the latency at or under which we don't
|
||||
// try to upgrade to a better path.
|
||||
goodEnoughLatency = 5 * time.Millisecond
|
||||
|
||||
// derpInactiveCleanupTime is how long a non-home DERP connection
|
||||
// needs to be idle (last written to) before we close it.
|
||||
derpInactiveCleanupTime = 60 * time.Second
|
||||
|
||||
// derpCleanStaleInterval is how often cleanStaleDerp runs when there
|
||||
// are potentially-stale DERP connections to close.
|
||||
derpCleanStaleInterval = 15 * time.Second
|
||||
|
||||
// endpointsFreshEnoughDuration is how long we consider a
|
||||
// STUN-derived endpoint valid for. UDP NAT mappings typically
|
||||
// expire at 30 seconds, so this is a few seconds shy of that.
|
||||
endpointsFreshEnoughDuration = 27 * time.Second
|
||||
)
|
||||
|
||||
// endpointState is some state and history for a specific endpoint of
|
||||
@@ -2865,6 +2970,10 @@ type endpointState struct {
|
||||
// updated and use it to discard old candidates.
|
||||
lastGotPing time.Time
|
||||
|
||||
// callMeMaybeTime, if non-zero, is the time this endpoint
|
||||
// was advertised last via a call-me-maybe disco message.
|
||||
callMeMaybeTime time.Time
|
||||
|
||||
recentPongs []pongReply // ring buffer up to pongHistoryCount entries
|
||||
recentPong uint16 // index into recentPongs of most recent; older before, wrapped
|
||||
|
||||
@@ -2878,11 +2987,13 @@ const indexSentinelDeleted = -1
|
||||
// shouldDeleteLocked reports whether we should delete this endpoint.
|
||||
func (st *endpointState) shouldDeleteLocked() bool {
|
||||
switch {
|
||||
case !st.callMeMaybeTime.IsZero():
|
||||
return false
|
||||
case st.lastGotPing.IsZero():
|
||||
// This was an endpoint from the network map. Is it still in the network map?
|
||||
return st.index == indexSentinelDeleted
|
||||
default:
|
||||
// Thiw was an endpoint discovered at runtime.
|
||||
// This was an endpoint discovered at runtime.
|
||||
return time.Since(st.lastGotPing) > sessionActiveTimeout
|
||||
}
|
||||
}
|
||||
@@ -2945,13 +3056,6 @@ func (de *discoEndpoint) String() string {
|
||||
return fmt.Sprintf("magicsock.discoEndpoint{%v, %v}", de.publicKey.ShortString(), de.discoShort)
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) Addrs() string {
|
||||
// This has to be the same string that was passed to
|
||||
// CreateEndpoint, otherwise Reconfig will end up recreating
|
||||
// Endpoints and losing state over time.
|
||||
return de.wgEndpointHostPort
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) ClearSrc() {}
|
||||
func (de *discoEndpoint) SrcToString() string { panic("unused") } // unused by wireguard-go
|
||||
func (de *discoEndpoint) SrcIP() net.IP { panic("unused") } // unused by wireguard-go
|
||||
@@ -2982,10 +3086,6 @@ func (de *discoEndpoint) heartbeat() {
|
||||
|
||||
de.heartBeatTimer = nil
|
||||
|
||||
if de.c.isClosed() {
|
||||
return
|
||||
}
|
||||
|
||||
if de.lastSend.IsZero() {
|
||||
// Shouldn't happen.
|
||||
return
|
||||
@@ -3200,13 +3300,12 @@ func (de *discoEndpoint) sendPingsLocked(now time.Time, sendCallMeMaybe bool) {
|
||||
}
|
||||
derpAddr := de.derpAddr
|
||||
if sentAny && sendCallMeMaybe && !derpAddr.IsZero() {
|
||||
// In just a bit of a time (for goroutines above to schedule and run),
|
||||
// send a message to peer via DERP informing them that we've sent
|
||||
// so our firewall ports are probably open and now would be a good time
|
||||
// for them to connect.
|
||||
time.AfterFunc(5*time.Millisecond, func() {
|
||||
de.sendDiscoMessage(derpAddr, disco.CallMeMaybe{}, discoLog)
|
||||
})
|
||||
// Have our magicsock.Conn figure out its STUN endpoint (if
|
||||
// it doesn't know already) and then send a CallMeMaybe
|
||||
// message to our peer via DERP informing them that we've
|
||||
// sent so our firewall ports are probably open and now
|
||||
// would be a good time for them to connect.
|
||||
go de.c.enqueueCallMeMaybe(derpAddr, de)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3388,10 +3487,55 @@ func (st *endpointState) addPongReplyLocked(r pongReply) {
|
||||
// DERP. The contract for use of this message is that the peer has
|
||||
// already sent to us via UDP, so their stateful firewall should be
|
||||
// open. Now we can Ping back and make it through.
|
||||
func (de *discoEndpoint) handleCallMeMaybe() {
|
||||
func (de *discoEndpoint) handleCallMeMaybe(m *disco.CallMeMaybe) {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for ep := range de.isCallMeMaybeEP {
|
||||
de.isCallMeMaybeEP[ep] = false // mark for deletion
|
||||
}
|
||||
if de.isCallMeMaybeEP == nil {
|
||||
de.isCallMeMaybeEP = map[netaddr.IPPort]bool{}
|
||||
}
|
||||
var newEPs []netaddr.IPPort
|
||||
for _, ep := range m.MyNumber {
|
||||
if ep.IP.Is6() && ep.IP.IsLinkLocalUnicast() {
|
||||
// We send these out, but ignore them for now.
|
||||
// TODO: teach the ping code to ping on all interfaces
|
||||
// for these.
|
||||
continue
|
||||
}
|
||||
de.isCallMeMaybeEP[ep] = true
|
||||
if es, ok := de.endpointState[ep]; ok {
|
||||
es.callMeMaybeTime = now
|
||||
} else {
|
||||
de.endpointState[ep] = &endpointState{callMeMaybeTime: now}
|
||||
newEPs = append(newEPs, ep)
|
||||
}
|
||||
}
|
||||
if len(newEPs) > 0 {
|
||||
de.c.logf("magicsock: disco: call-me-maybe from %v %v added new endpoints: %v",
|
||||
de.publicKey.ShortString(), de.discoShort,
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
for i, ep := range newEPs {
|
||||
if i > 0 {
|
||||
w.WriteString(", ")
|
||||
}
|
||||
w.WriteString(ep.String())
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Delete any prior CalllMeMaybe endpoints that weren't included
|
||||
// in this message.
|
||||
for ep, want := range de.isCallMeMaybeEP {
|
||||
if !want {
|
||||
delete(de.isCallMeMaybeEP, ep)
|
||||
de.deleteEndpointLocked(ep)
|
||||
}
|
||||
}
|
||||
|
||||
// Zero out all the lastPing times to force sendPingsLocked to send new ones,
|
||||
// even if it's been less than 5 seconds ago.
|
||||
for _, st := range de.endpointState {
|
||||
@@ -3420,6 +3564,7 @@ func (de *discoEndpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
// It's called when a discovery endpoint is no longer present in the NetworkMap,
|
||||
// or when magicsock is transition from running to stopped state (via SetPrivateKey(zero))
|
||||
func (de *discoEndpoint) stopAndReset() {
|
||||
atomic.AddInt64(&de.numStopAndResetAtomic, 1)
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
@@ -3447,6 +3592,10 @@ func (de *discoEndpoint) stopAndReset() {
|
||||
de.pendingCLIPings = nil
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) numStopAndReset() int64 {
|
||||
return atomic.LoadInt64(&de.numStopAndResetAtomic)
|
||||
}
|
||||
|
||||
// derpStr replaces DERP IPs in s with "derp-".
|
||||
func derpStr(s string) string { return strings.ReplaceAll(s, "127.3.3.40:", "derp-") }
|
||||
|
||||
@@ -3468,3 +3617,11 @@ func (c *Conn) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok
|
||||
}
|
||||
return nil, u, false
|
||||
}
|
||||
|
||||
// ippEndpointCache is a mutex-free single-element cache, mapping from
|
||||
// a single netaddr.IPPort to a single endpoint.
|
||||
type ippEndpointCache struct {
|
||||
ipp netaddr.IPPort
|
||||
gen int64
|
||||
de *discoEndpoint
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun/tuntest"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
@@ -46,6 +45,8 @@ import (
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/tstun"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wglog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -127,6 +128,7 @@ type magicStack struct {
|
||||
tun *tuntest.ChannelTUN // TUN device to send/receive packets
|
||||
tsTun *tstun.TUN // wrapped tun that implements filtering and wgengine hooks
|
||||
dev *device.Device // the wireguard-go Device that connects the previous things
|
||||
wgLogger *wglog.Logger // wireguard-go log wrapper
|
||||
}
|
||||
|
||||
// newMagicStack builds and initializes an idle magicsock and
|
||||
@@ -163,8 +165,9 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
|
||||
tsTun := tstun.WrapTUN(logf, tun.TUN())
|
||||
tsTun.SetFilter(filter.NewAllowAllForTest(logf))
|
||||
|
||||
wgLogger := wglog.NewLogger(logf)
|
||||
dev := device.NewDevice(tsTun, &device.DeviceOptions{
|
||||
Logger: wireguardGoLogger(logf),
|
||||
Logger: wgLogger.DeviceLogger,
|
||||
CreateEndpoint: conn.CreateEndpoint,
|
||||
CreateBind: conn.CreateBind,
|
||||
SkipBindUpdate: true,
|
||||
@@ -187,9 +190,15 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
|
||||
tun: tun,
|
||||
tsTun: tsTun,
|
||||
dev: dev,
|
||||
wgLogger: wgLogger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *magicStack) Reconfig(cfg *wgcfg.Config) error {
|
||||
s.wgLogger.SetPeers(cfg.Peers)
|
||||
return wgcfg.ReconfigDevice(s.dev, cfg, s.conn.logf)
|
||||
}
|
||||
|
||||
func (s *magicStack) String() string {
|
||||
pub := s.Public()
|
||||
return pub.ShortString()
|
||||
@@ -290,7 +299,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
|
||||
// blow up. Shouldn't happen anyway.
|
||||
panic(fmt.Sprintf("failed to construct wgcfg from netmap: %v", err))
|
||||
}
|
||||
if err := m.dev.Reconfig(wg); err != nil {
|
||||
if err := m.Reconfig(wg); err != nil {
|
||||
panic(fmt.Sprintf("device reconfig failed: %v", err))
|
||||
}
|
||||
}
|
||||
@@ -526,7 +535,7 @@ func TestDeviceStartStop(t *testing.T) {
|
||||
|
||||
tun := tuntest.NewChannelTUN()
|
||||
dev := device.NewDevice(tun.TUN(), &device.DeviceOptions{
|
||||
Logger: wireguardGoLogger(t.Logf),
|
||||
Logger: wglog.NewLogger(t.Logf).DeviceLogger,
|
||||
CreateEndpoint: conn.CreateEndpoint,
|
||||
CreateBind: conn.CreateBind,
|
||||
SkipBindUpdate: true,
|
||||
@@ -896,7 +905,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
|
||||
// This gets reassigned inside every test, so that the connections
|
||||
// all log using the "current" t.Logf function. Sigh.
|
||||
logf, setT := makeNestable(t)
|
||||
nestedLogf, setT := makeNestable(t)
|
||||
|
||||
logf, closeLogf := logger.LogfCloser(nestedLogf)
|
||||
defer closeLogf()
|
||||
|
||||
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
|
||||
defer cleanup()
|
||||
@@ -912,10 +924,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
}
|
||||
cfgs := makeConfigs(t, addrs)
|
||||
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
|
||||
if err := m2.Reconfig(&cfgs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -980,7 +992,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
t.Run("no-op dev1 reconfig", func(t *testing.T) {
|
||||
setT(t)
|
||||
defer setT(outerT)
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ping1(t)
|
||||
@@ -1057,10 +1069,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
ep1 := cfgs[1].Peers[0].Endpoints
|
||||
ep1 = derpEp + "," + ep1
|
||||
cfgs[1].Peers[0].Endpoints = ep1
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
|
||||
if err := m2.Reconfig(&cfgs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1073,10 +1085,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
// Disable real route.
|
||||
cfgs[0].Peers[0].Endpoints = derpEp
|
||||
cfgs[1].Peers[0].Endpoints = derpEp
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
|
||||
if err := m2.Reconfig(&cfgs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond) // TODO remove
|
||||
@@ -1102,10 +1114,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
if ep2 := cfgs[1].Peers[0].Endpoints; len(ep2) != 1 {
|
||||
t.Errorf("unexpected peer endpoints in dev2: %v", ep2)
|
||||
}
|
||||
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
|
||||
if err := m2.Reconfig(&cfgs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
|
||||
if err := m1.Reconfig(&cfgs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Dear future human debugging a test failure here: this test is
|
||||
@@ -1119,7 +1131,11 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
defer setT(outerT)
|
||||
pingSeq(t, 50, 700*time.Millisecond, false)
|
||||
|
||||
ep2 := m2.dev.Config().Peers[0].Endpoints
|
||||
cfg, err := wgcfg.DeviceConfig(m2.dev)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ep2 := cfg.Peers[0].Endpoints
|
||||
if len(ep2) != 2 {
|
||||
t.Error("handshake spray failed to find real route")
|
||||
}
|
||||
@@ -1484,19 +1500,3 @@ func BenchmarkReceiveFrom_Native(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func wireguardGoLogger(logf logger.Logf) *device.Logger {
|
||||
// wireguard-go logs as it starts and stops routines.
|
||||
// Silence those; there are a lot of them, and they're just noise.
|
||||
allowLogf := func(s string) bool {
|
||||
return !strings.Contains(s, "Routine:")
|
||||
}
|
||||
filtered := logger.Filtered(logf, allowLogf)
|
||||
stdLogger := logger.StdLogger(filtered)
|
||||
|
||||
return &device.Logger{
|
||||
Debug: stdLogger,
|
||||
Info: stdLogger,
|
||||
Error: stdLogger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.I
|
||||
return nil, fmt.Errorf("interfaceFromLUID: interface with LUID %v not found", luid)
|
||||
}
|
||||
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
||||
const mtu = 0
|
||||
luid := winipcfg.LUID(tun.LUID())
|
||||
iface, err := interfaceFromLUID(luid,
|
||||
@@ -251,6 +251,15 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send non-nil return errors to retErrc, to interupt our background
|
||||
// setPrivateNetwork goroutine.
|
||||
retErrc := make(chan error, 1)
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
retErrc <- retErr
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
// It takes a weirdly long time for Windows to notice the
|
||||
// new interface has come up. Poll periodically until it
|
||||
@@ -262,11 +271,18 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
log.Printf("setPrivateNetwork(try=%d): %v", i, err)
|
||||
} else {
|
||||
if found {
|
||||
if i > 0 {
|
||||
log.Printf("setPrivateNetwork(try=%d): success", i)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("setPrivateNetwork(try=%d): not found", i)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
case <-retErrc:
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Printf("setPrivateNetwork: adapter LUID %v not found after %d tries, giving up", luid, tries)
|
||||
}()
|
||||
|
||||
@@ -366,7 +366,9 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error {
|
||||
// address is already assigned to the interface, or if the addition
|
||||
// fails.
|
||||
func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
|
||||
|
||||
if !r.v6Available && addr.IP.Is6() {
|
||||
return nil
|
||||
}
|
||||
if err := r.cmd.run("ip", "addr", "add", addr.String(), "dev", r.tunname); err != nil {
|
||||
return fmt.Errorf("adding address %q to tunnel interface: %w", addr, err)
|
||||
}
|
||||
@@ -380,6 +382,9 @@ func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
|
||||
// the address is not assigned to the interface, or if the removal
|
||||
// fails.
|
||||
func (r *linuxRouter) delAddress(addr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && addr.IP.Is6() {
|
||||
return nil
|
||||
}
|
||||
if err := r.delLoopbackRule(addr.IP); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -437,6 +442,9 @@ func (r *linuxRouter) delLoopbackRule(addr netaddr.IP) error {
|
||||
// interface. Fails if the route already exists, or if adding the
|
||||
// route fails.
|
||||
func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && cidr.IP.Is6() {
|
||||
return nil
|
||||
}
|
||||
args := []string{
|
||||
"ip", "route", "add",
|
||||
normalizeCIDR(cidr),
|
||||
@@ -452,6 +460,9 @@ func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error {
|
||||
// interface. Fails if the route doesn't exist, or if removing the
|
||||
// route fails.
|
||||
func (r *linuxRouter) delRoute(cidr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && cidr.IP.Is6() {
|
||||
return nil
|
||||
}
|
||||
args := []string{
|
||||
"ip", "route", "del",
|
||||
normalizeCIDR(cidr),
|
||||
|
||||
@@ -346,8 +346,8 @@ func (t *TUN) filterIn(buf []byte) filter.Response {
|
||||
}
|
||||
|
||||
if t.PostFilterIn != nil {
|
||||
if t.PostFilterIn(p, t) == filter.Drop {
|
||||
return filter.Drop
|
||||
if res := t.PostFilterIn(p, t); res.IsDrop() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -24,7 +23,6 @@ import (
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
@@ -47,6 +45,8 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/tstun"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wglog"
|
||||
)
|
||||
|
||||
// minimalMTU is the MTU we set on tailscale's TUN
|
||||
@@ -84,6 +84,7 @@ const (
|
||||
|
||||
type userspaceEngine struct {
|
||||
logf logger.Logf
|
||||
wgLogger *wglog.Logger //a wireguard-go logging wrapper
|
||||
reqCh chan struct{}
|
||||
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
|
||||
timeNow func() time.Time
|
||||
@@ -110,6 +111,7 @@ type userspaceEngine struct {
|
||||
trimmedDisco map[tailcfg.DiscoKey]bool // set of disco keys of peers currently excluded from wireguard config
|
||||
sentActivityAt map[netaddr.IP]*int64 // value is atomic int64 of unixtime
|
||||
destIPActivityFuncs map[netaddr.IP]func()
|
||||
statusBufioReader *bufio.Reader // reusable for UAPI
|
||||
|
||||
mu sync.Mutex // guards following; see lock order comment below
|
||||
closing bool // Close was called (even if we're still closing)
|
||||
@@ -192,6 +194,7 @@ func NewUserspaceEngine(logf logger.Logf, tunname string, listenPort uint16) (En
|
||||
|
||||
e, err := NewUserspaceEngineAdvanced(conf)
|
||||
if err != nil {
|
||||
tun.Close()
|
||||
return nil, err
|
||||
}
|
||||
return e, err
|
||||
@@ -279,23 +282,9 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
e.tundev.PostFilterOut = e.trackOpenPostFilterOut
|
||||
}
|
||||
|
||||
// wireguard-go logs as it starts and stops routines.
|
||||
// Silence those; there are a lot of them, and they're just noise.
|
||||
allowLogf := func(s string) bool {
|
||||
return !strings.Contains(s, "Routine:")
|
||||
}
|
||||
filtered := logger.Filtered(logf, allowLogf)
|
||||
// flags==0 because logf is already nested in another logger.
|
||||
// The outer one can display the preferred log prefixes, etc.
|
||||
dlog := logger.StdLogger(filtered)
|
||||
logger := device.Logger{
|
||||
Debug: dlog,
|
||||
Info: dlog,
|
||||
Error: dlog,
|
||||
}
|
||||
|
||||
e.wgLogger = wglog.NewLogger(logf)
|
||||
opts := &device.DeviceOptions{
|
||||
Logger: &logger,
|
||||
Logger: e.wgLogger.DeviceLogger,
|
||||
HandshakeDone: func(peerKey device.NoisePublicKey, peer *device.Peer, deviceAllowedIPs *device.AllowedIPs) {
|
||||
// Send an unsolicited status event every time a
|
||||
// handshake completes. This makes sure our UI can
|
||||
@@ -774,6 +763,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
|
||||
full := e.lastCfgFull
|
||||
e.wgLogger.SetPeers(full.Peers)
|
||||
|
||||
// Compute a minimal config to pass to wireguard-go
|
||||
// based on the full config. Prune off all the peers
|
||||
@@ -846,7 +836,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
if numRemove > 0 {
|
||||
e.logf("wgengine: Reconfig: removing session keys for %d peers", numRemove)
|
||||
if err := e.wgdev.Reconfig(&minner); err != nil {
|
||||
if err := wgcfg.ReconfigDevice(e.wgdev, &minner, e.logf); err != nil {
|
||||
e.logf("wgdev.Reconfig: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -854,7 +844,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
|
||||
e.logf("wgengine: Reconfig: configuring userspace wireguard config (with %d/%d peers)", len(min.Peers), len(full.Peers))
|
||||
if err := e.wgdev.Reconfig(&min); err != nil {
|
||||
if err := wgcfg.ReconfigDevice(e.wgdev, &min, e.logf); err != nil {
|
||||
e.logf("wgdev.Reconfig: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -1046,8 +1036,8 @@ func (e *userspaceEngine) getStatusCallback() StatusCallback {
|
||||
return e.statusCallback
|
||||
}
|
||||
|
||||
// TODO: this function returns an error but it's always nil, and when
|
||||
// there's actually a problem it just calls log.Fatal. Why?
|
||||
var singleNewline = []byte{'\n'}
|
||||
|
||||
func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
// Grab derpConns before acquiring wgLock to not violate lock ordering;
|
||||
// the DERPs method acquires magicsock.Conn.mu.
|
||||
@@ -1072,15 +1062,12 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// lineLen is the max UAPI line we expect. The longest I see is
|
||||
// len("preshared_key=")+64 hex+"\n" == 79. Add some slop.
|
||||
const lineLen = 100
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close() // to unblock writes on error path returns
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
bw := bufio.NewWriterSize(pw, lineLen)
|
||||
// TODO(apenwarr): get rid of silly uapi stuff for in-process comms
|
||||
// FIXME: get notified of status changes instead of polling.
|
||||
filter := device.IPCGetFilter{
|
||||
@@ -1088,23 +1075,34 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
// unused below; request that they not be sent instead.
|
||||
FilterAllowedIPs: true,
|
||||
}
|
||||
if err := e.wgdev.IpcGetOperationFiltered(bw, filter); err != nil {
|
||||
errc <- fmt.Errorf("IpcGetOperation: %w", err)
|
||||
return
|
||||
err := e.wgdev.IpcGetOperationFiltered(pw, filter)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("IpcGetOperation: %w", err)
|
||||
}
|
||||
errc <- bw.Flush()
|
||||
errc <- err
|
||||
}()
|
||||
|
||||
pp := make(map[wgkey.Key]*PeerStatus)
|
||||
p := &PeerStatus{}
|
||||
|
||||
var hst1, hst2, n int64
|
||||
var err error
|
||||
|
||||
bs := bufio.NewScanner(pr)
|
||||
bs.Buffer(make([]byte, lineLen), lineLen)
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
br := e.statusBufioReader
|
||||
if br != nil {
|
||||
br.Reset(pr)
|
||||
} else {
|
||||
br = bufio.NewReaderSize(pr, 1<<10)
|
||||
e.statusBufioReader = br
|
||||
}
|
||||
for {
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading from UAPI pipe: %w", err)
|
||||
}
|
||||
line = bytes.TrimSuffix(line, singleNewline)
|
||||
k := line
|
||||
var v mem.RO
|
||||
if i := bytes.IndexByte(line, '='); i != -1 {
|
||||
@@ -1115,7 +1113,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
case "public_key":
|
||||
pk, err := key.NewPublicFromHexMem(v)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: invalid key %#v", v)
|
||||
return nil, fmt.Errorf("IpcGetOperation: invalid key in line %q", line)
|
||||
}
|
||||
p = &PeerStatus{}
|
||||
pp[wgkey.Key(pk)] = p
|
||||
@@ -1126,34 +1124,31 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
n, err = mem.ParseInt(v, 10, 64)
|
||||
p.RxBytes = ByteCount(n)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: rx_bytes invalid: %#v", line)
|
||||
return nil, fmt.Errorf("IpcGetOperation: rx_bytes invalid: %#v", line)
|
||||
}
|
||||
case "tx_bytes":
|
||||
n, err = mem.ParseInt(v, 10, 64)
|
||||
p.TxBytes = ByteCount(n)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: tx_bytes invalid: %#v", line)
|
||||
return nil, fmt.Errorf("IpcGetOperation: tx_bytes invalid: %#v", line)
|
||||
}
|
||||
case "last_handshake_time_sec":
|
||||
hst1, err = mem.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: hst1 invalid: %#v", line)
|
||||
return nil, fmt.Errorf("IpcGetOperation: hst1 invalid: %#v", line)
|
||||
}
|
||||
case "last_handshake_time_nsec":
|
||||
hst2, err = mem.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("IpcGetOperation: hst2 invalid: %#v", line)
|
||||
return nil, fmt.Errorf("IpcGetOperation: hst2 invalid: %#v", line)
|
||||
}
|
||||
if hst1 != 0 || hst2 != 0 {
|
||||
p.LastHandshake = time.Unix(hst1, hst2)
|
||||
} // else leave at time.IsZero()
|
||||
}
|
||||
}
|
||||
if err := bs.Err(); err != nil {
|
||||
log.Fatalf("reading IpcGetOperation output: %v", err)
|
||||
}
|
||||
if err := <-errc; err != nil {
|
||||
log.Fatalf("IpcGetOperation: %v", err)
|
||||
return nil, fmt.Errorf("IpcGetOperation: %v", err)
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tstun"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestNoteReceiveActivity(t *testing.T) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -21,6 +20,7 @@ import (
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
// NewWatchdog wraps an Engine and makes sure that all methods complete
|
||||
|
||||
67
wgengine/wgcfg/config.go
Normal file
67
wgengine/wgcfg/config.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package wgcfg has types and a parser for representing WireGuard config.
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Config is a WireGuard configuration.
|
||||
// It only supports the set of things Tailscale uses.
|
||||
type Config struct {
|
||||
Name string
|
||||
PrivateKey PrivateKey
|
||||
Addresses []netaddr.IPPrefix
|
||||
ListenPort uint16
|
||||
MTU uint16
|
||||
DNS []netaddr.IP
|
||||
Peers []Peer
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
PublicKey Key
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints string // comma-separated host/port pairs: "1.2.3.4:56,[::]:80"
|
||||
PersistentKeepalive uint16
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of Config.
|
||||
// The result aliases no memory with the original.
|
||||
func (cfg Config) Copy() Config {
|
||||
res := cfg
|
||||
if res.Addresses != nil {
|
||||
res.Addresses = append([]netaddr.IPPrefix{}, res.Addresses...)
|
||||
}
|
||||
if res.DNS != nil {
|
||||
res.DNS = append([]netaddr.IP{}, res.DNS...)
|
||||
}
|
||||
peers := make([]Peer, 0, len(res.Peers))
|
||||
for _, peer := range res.Peers {
|
||||
peers = append(peers, peer.Copy())
|
||||
}
|
||||
res.Peers = peers
|
||||
return res
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of Peer.
|
||||
// The result aliases no memory with the original.
|
||||
func (peer Peer) Copy() Peer {
|
||||
res := peer
|
||||
if res.AllowedIPs != nil {
|
||||
res.AllowedIPs = append([]netaddr.IPPrefix{}, res.AllowedIPs...)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// PeerWithKey returns the Peer with key k and reports whether it was found.
|
||||
func (config Config) PeerWithKey(k Key) (Peer, bool) {
|
||||
for _, p := range config.Peers {
|
||||
if p.PublicKey == k {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return Peer{}, false
|
||||
}
|
||||
61
wgengine/wgcfg/device.go
Normal file
61
wgengine/wgcfg/device.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func DeviceConfig(d *device.Device) (*Config, error) {
|
||||
r, w := io.Pipe()
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
errc <- d.IpcGetOperation(w)
|
||||
w.Close()
|
||||
}()
|
||||
cfg, err := FromUAPI(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := <-errc; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(cfg.Peers, func(i, j int) bool {
|
||||
return cfg.Peers[i].PublicKey.LessThan(&cfg.Peers[j].PublicKey)
|
||||
})
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// ReconfigDevice replaces the existing device configuration with cfg.
|
||||
func ReconfigDevice(d *device.Device, cfg *Config, logf logger.Logf) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logf("wgcfg.Reconfig failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
prev, err := DeviceConfig(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, w := io.Pipe()
|
||||
errc := make(chan error)
|
||||
go func() {
|
||||
errc <- d.IpcSetOperation(r)
|
||||
}()
|
||||
|
||||
err = cfg.ToUAPI(w, prev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Close()
|
||||
return <-errc
|
||||
}
|
||||
242
wgengine/wgcfg/device_test.go
Normal file
242
wgengine/wgcfg/device_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestDeviceConfig(t *testing.T) {
|
||||
newPrivateKey := func() (Key, PrivateKey) {
|
||||
t.Helper()
|
||||
pk, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Key(pk.Public()), PrivateKey(pk)
|
||||
}
|
||||
k1, pk1 := newPrivateKey()
|
||||
ip1 := netaddr.MustParseIPPrefix("10.0.0.1/32")
|
||||
|
||||
k2, pk2 := newPrivateKey()
|
||||
ip2 := netaddr.MustParseIPPrefix("10.0.0.2/32")
|
||||
|
||||
k3, _ := newPrivateKey()
|
||||
ip3 := netaddr.MustParseIPPrefix("10.0.0.3/32")
|
||||
|
||||
cfg1 := &Config{
|
||||
PrivateKey: PrivateKey(pk1),
|
||||
Peers: []Peer{{
|
||||
PublicKey: k2,
|
||||
AllowedIPs: []netaddr.IPPrefix{ip2},
|
||||
}},
|
||||
}
|
||||
|
||||
cfg2 := &Config{
|
||||
PrivateKey: PrivateKey(pk2),
|
||||
Peers: []Peer{{
|
||||
PublicKey: k1,
|
||||
AllowedIPs: []netaddr.IPPrefix{ip1},
|
||||
PersistentKeepalive: 5,
|
||||
}},
|
||||
}
|
||||
|
||||
device1 := device.NewDevice(newNilTun(), &device.DeviceOptions{
|
||||
Logger: device.NewLogger(device.LogLevelError, "device1"),
|
||||
})
|
||||
device2 := device.NewDevice(newNilTun(), &device.DeviceOptions{
|
||||
Logger: device.NewLogger(device.LogLevelError, "device2"),
|
||||
})
|
||||
defer device1.Close()
|
||||
defer device2.Close()
|
||||
|
||||
cmp := func(t *testing.T, d *device.Device, want *Config) {
|
||||
t.Helper()
|
||||
got, err := DeviceConfig(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prev := new(Config)
|
||||
gotbuf := new(strings.Builder)
|
||||
err = got.ToUAPI(gotbuf, prev)
|
||||
gotStr := gotbuf.String()
|
||||
if err != nil {
|
||||
t.Errorf("got.ToUAPI(): error: %v", err)
|
||||
return
|
||||
}
|
||||
wantbuf := new(strings.Builder)
|
||||
err = want.ToUAPI(wantbuf, prev)
|
||||
wantStr := wantbuf.String()
|
||||
if err != nil {
|
||||
t.Errorf("want.ToUAPI(): error: %v", err)
|
||||
return
|
||||
}
|
||||
if gotStr != wantStr {
|
||||
buf := new(bytes.Buffer)
|
||||
w := bufio.NewWriter(buf)
|
||||
if err := d.IpcGetOperation(w); err != nil {
|
||||
t.Errorf("on error, could not IpcGetOperation: %v", err)
|
||||
}
|
||||
w.Flush()
|
||||
t.Errorf("cfg:\n%s\n---- want:\n%s\n---- uapi:\n%s", gotStr, wantStr, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("device1 config", func(t *testing.T) {
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
})
|
||||
|
||||
t.Run("device2 config", func(t *testing.T) {
|
||||
if err := ReconfigDevice(device2, cfg2, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device2, cfg2)
|
||||
})
|
||||
|
||||
// This is only to test that Config and Reconfig are properly synchronized.
|
||||
t.Run("device2 config/reconfig", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
ReconfigDevice(device2, cfg2, t.Logf)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
DeviceConfig(device2)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("device1 modify peer", func(t *testing.T) {
|
||||
cfg1.Peers[0].Endpoints = "1.2.3.4:12345"
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
})
|
||||
|
||||
t.Run("device1 replace endpoint", func(t *testing.T) {
|
||||
cfg1.Peers[0].Endpoints = "1.1.1.1:123"
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
})
|
||||
|
||||
t.Run("device1 add new peer", func(t *testing.T) {
|
||||
cfg1.Peers = append(cfg1.Peers, Peer{
|
||||
PublicKey: k3,
|
||||
AllowedIPs: []netaddr.IPPrefix{ip3},
|
||||
})
|
||||
sort.Slice(cfg1.Peers, func(i, j int) bool {
|
||||
return cfg1.Peers[i].PublicKey.LessThan(&cfg1.Peers[j].PublicKey)
|
||||
})
|
||||
|
||||
origCfg, err := DeviceConfig(device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
|
||||
newCfg, err := DeviceConfig(device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
peer0 := func(cfg *Config) Peer {
|
||||
p, ok := cfg.PeerWithKey(k2)
|
||||
if !ok {
|
||||
t.Helper()
|
||||
t.Fatal("failed to look up peer 2")
|
||||
}
|
||||
return p
|
||||
}
|
||||
peersEqual := func(p, q Peer) bool {
|
||||
return p.PublicKey == q.PublicKey && p.PersistentKeepalive == q.PersistentKeepalive &&
|
||||
p.Endpoints == q.Endpoints && cidrsEqual(p.AllowedIPs, q.AllowedIPs)
|
||||
}
|
||||
if !peersEqual(peer0(origCfg), peer0(newCfg)) {
|
||||
t.Error("reconfig modified old peer")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("device1 remove peer", func(t *testing.T) {
|
||||
removeKey := cfg1.Peers[len(cfg1.Peers)-1].PublicKey
|
||||
cfg1.Peers = cfg1.Peers[:len(cfg1.Peers)-1]
|
||||
|
||||
if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmp(t, device1, cfg1)
|
||||
|
||||
newCfg, err := DeviceConfig(device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, ok := newCfg.PeerWithKey(removeKey)
|
||||
if ok {
|
||||
t.Error("reconfig failed to remove peer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: replace with a loopback tunnel
|
||||
type nilTun struct {
|
||||
events chan tun.Event
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func newNilTun() tun.Device {
|
||||
return &nilTun{
|
||||
events: make(chan tun.Event),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *nilTun) File() *os.File { return nil }
|
||||
func (t *nilTun) Flush() error { return nil }
|
||||
func (t *nilTun) MTU() (int, error) { return 1420, nil }
|
||||
func (t *nilTun) Name() (string, error) { return "niltun", nil }
|
||||
func (t *nilTun) Events() chan tun.Event { return t.events }
|
||||
|
||||
func (t *nilTun) Read(data []byte, offset int) (int, error) {
|
||||
<-t.closed
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (t *nilTun) Write(data []byte, offset int) (int, error) {
|
||||
<-t.closed
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (t *nilTun) Close() error {
|
||||
close(t.events)
|
||||
close(t.closed)
|
||||
return nil
|
||||
}
|
||||
240
wgengine/wgcfg/key.go
Normal file
240
wgengine/wgcfg/key.go
Normal file
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const KeySize = 32
|
||||
|
||||
// Key is curve25519 key.
|
||||
// It is used by WireGuard to represent public and preshared keys.
|
||||
type Key [KeySize]byte
|
||||
|
||||
// NewPresharedKey generates a new random key.
|
||||
func NewPresharedKey() (*Key, error) {
|
||||
var k [KeySize]byte
|
||||
_, err := rand.Read(k[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return (*Key)(&k), nil
|
||||
}
|
||||
|
||||
func ParseKey(b64 string) (*Key, error) { return parseKeyBase64(base64.StdEncoding, b64) }
|
||||
|
||||
func ParseHexKey(s string) (Key, error) {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return Key{}, &ParseError{"invalid hex key: " + err.Error(), s}
|
||||
}
|
||||
if len(b) != KeySize {
|
||||
return Key{}, &ParseError{fmt.Sprintf("invalid hex key length: %d", len(b)), s}
|
||||
}
|
||||
|
||||
var key Key
|
||||
copy(key[:], b)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func ParsePrivateHexKey(v string) (PrivateKey, error) {
|
||||
k, err := ParseHexKey(v)
|
||||
if err != nil {
|
||||
return PrivateKey{}, err
|
||||
}
|
||||
pk := PrivateKey(k)
|
||||
if pk.IsZero() {
|
||||
// Do not clamp a zero key, pass the zero through
|
||||
// (much like NaN propagation) so that IsZero reports
|
||||
// a useful result.
|
||||
return pk, nil
|
||||
}
|
||||
pk.clamp()
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
func (k Key) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k Key) String() string { return k.ShortString() }
|
||||
func (k Key) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *Key) ShortString() string {
|
||||
long := k.Base64()
|
||||
return "[" + long[0:5] + "]"
|
||||
}
|
||||
|
||||
func (k *Key) IsZero() bool {
|
||||
if k == nil {
|
||||
return true
|
||||
}
|
||||
var zeros Key
|
||||
return subtle.ConstantTimeCompare(zeros[:], k[:]) == 1
|
||||
}
|
||||
|
||||
func (k *Key) MarshalJSON() ([]byte, error) {
|
||||
if k == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `"%x"`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *Key) UnmarshalJSON(b []byte) error {
|
||||
if k == nil {
|
||||
return errors.New("wgcfg.Key: UnmarshalJSON on nil pointer")
|
||||
}
|
||||
if len(b) < 3 || b[0] != '"' || b[len(b)-1] != '"' {
|
||||
return errors.New("wgcfg.Key: UnmarshalJSON not given a string")
|
||||
}
|
||||
b = b[1 : len(b)-1]
|
||||
key, err := ParseHexKey(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgcfg.Key: UnmarshalJSON: %v", err)
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Key) LessThan(b *Key) bool {
|
||||
for i := range a {
|
||||
if a[i] < b[i] {
|
||||
return true
|
||||
} else if a[i] > b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PrivateKey is curve25519 key.
|
||||
// It is used by WireGuard to represent private keys.
|
||||
type PrivateKey [KeySize]byte
|
||||
|
||||
// NewPrivateKey generates a new curve25519 secret key.
|
||||
// It conforms to the format described on https://cr.yp.to/ecdh.html.
|
||||
func NewPrivateKey() (PrivateKey, error) {
|
||||
k, err := NewPresharedKey()
|
||||
if err != nil {
|
||||
return PrivateKey{}, err
|
||||
}
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
return (PrivateKey)(*k), nil
|
||||
}
|
||||
|
||||
func ParsePrivateKey(b64 string) (*PrivateKey, error) {
|
||||
k, err := parseKeyBase64(base64.StdEncoding, b64)
|
||||
return (*PrivateKey)(k), err
|
||||
}
|
||||
|
||||
func (k *PrivateKey) String() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k *PrivateKey) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k *PrivateKey) Equal(k2 PrivateKey) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *PrivateKey) IsZero() bool {
|
||||
pk := Key(*k)
|
||||
return pk.IsZero()
|
||||
}
|
||||
|
||||
func (k *PrivateKey) clamp() {
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
}
|
||||
|
||||
// Public computes the public key matching this curve25519 secret key.
|
||||
func (k *PrivateKey) Public() Key {
|
||||
pk := Key(*k)
|
||||
if pk.IsZero() {
|
||||
panic("Tried to generate emptyPrivateKey.Public()")
|
||||
}
|
||||
var p [KeySize]byte
|
||||
curve25519.ScalarBaseMult(&p, (*[KeySize]byte)(k))
|
||||
return (Key)(p)
|
||||
}
|
||||
|
||||
func (k PrivateKey) MarshalText() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `privkey:%x`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *PrivateKey) UnmarshalText(b []byte) error {
|
||||
s := string(b)
|
||||
if !strings.HasPrefix(s, `privkey:`) {
|
||||
return errors.New("wgcfg.PrivateKey: UnmarshalText not given a private-key string")
|
||||
}
|
||||
s = strings.TrimPrefix(s, `privkey:`)
|
||||
key, err := ParseHexKey(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgcfg.PrivateKey: UnmarshalText: %v", err)
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k PrivateKey) SharedSecret(pub Key) (ss [KeySize]byte) {
|
||||
apk := (*[KeySize]byte)(&pub)
|
||||
ask := (*[KeySize]byte)(&k)
|
||||
curve25519.ScalarMult(&ss, ask, apk) //lint:ignore SA1019 Jason says this is OK; match wireguard-go exactyl
|
||||
return ss
|
||||
}
|
||||
|
||||
func parseKeyBase64(enc *base64.Encoding, s string) (*Key, error) {
|
||||
k, err := enc.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, &ParseError{"Invalid key: " + err.Error(), s}
|
||||
}
|
||||
if len(k) != KeySize {
|
||||
return nil, &ParseError{"Keys must decode to exactly 32 bytes", s}
|
||||
}
|
||||
var key Key
|
||||
copy(key[:], k)
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func ParseSymmetricKey(b64 string) (SymmetricKey, error) {
|
||||
k, err := parseKeyBase64(base64.StdEncoding, b64)
|
||||
if err != nil {
|
||||
return SymmetricKey{}, err
|
||||
}
|
||||
return SymmetricKey(*k), nil
|
||||
}
|
||||
|
||||
func ParseSymmetricHexKey(s string) (SymmetricKey, error) {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return SymmetricKey{}, &ParseError{"invalid symmetric hex key: " + err.Error(), s}
|
||||
}
|
||||
if len(b) != chacha20poly1305.KeySize {
|
||||
return SymmetricKey{}, &ParseError{fmt.Sprintf("invalid symmetric hex key length: %d", len(b)), s}
|
||||
}
|
||||
var key SymmetricKey
|
||||
copy(key[:], b)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// SymmetricKey is a chacha20poly1305 key.
|
||||
// It is used by WireGuard to represent pre-shared symmetric keys.
|
||||
type SymmetricKey [chacha20poly1305.KeySize]byte
|
||||
|
||||
func (k SymmetricKey) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k SymmetricKey) String() string { return "sym:" + k.Base64()[:8] }
|
||||
func (k SymmetricKey) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k SymmetricKey) IsZero() bool { return k.Equal(SymmetricKey{}) }
|
||||
func (k SymmetricKey) Equal(k2 SymmetricKey) bool {
|
||||
return subtle.ConstantTimeCompare(k[:], k2[:]) == 1
|
||||
}
|
||||
111
wgengine/wgcfg/key_test.go
Normal file
111
wgengine/wgcfg/key_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeyBasics(t *testing.T) {
|
||||
k1, err := NewPresharedKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := k1.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("JSON round-trip", func(t *testing.T) {
|
||||
// should preserve the keys
|
||||
k2 := new(Key)
|
||||
if err := k2.UnmarshalJSON(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(k1[:], k2[:]) {
|
||||
t.Fatalf("k1 %v != k2 %v", k1[:], k2[:])
|
||||
}
|
||||
if b1, b2 := k1.String(), k2.String(); b1 != b2 {
|
||||
t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON incompatible with PrivateKey", func(t *testing.T) {
|
||||
k2 := new(PrivateKey)
|
||||
if err := k2.UnmarshalText(b); err == nil {
|
||||
t.Fatalf("successfully decoded key as private key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second key", func(t *testing.T) {
|
||||
// A second call to NewPresharedKey should make a new key.
|
||||
k3, err := NewPresharedKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(k1[:], k3[:]) {
|
||||
t.Fatalf("k1 %v == k3 %v", k1[:], k3[:])
|
||||
}
|
||||
// Check for obvious comparables to make sure we are not generating bad strings somewhere.
|
||||
if b1, b2 := k1.String(), k3.String(); b1 == b2 {
|
||||
t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
|
||||
}
|
||||
})
|
||||
}
|
||||
func TestPrivateKeyBasics(t *testing.T) {
|
||||
pri, err := NewPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := pri.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("JSON round-trip", func(t *testing.T) {
|
||||
// should preserve the keys
|
||||
pri2 := new(PrivateKey)
|
||||
if err := pri2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(pri[:], pri2[:]) {
|
||||
t.Fatalf("pri %v != pri2 %v", pri[:], pri2[:])
|
||||
}
|
||||
if b1, b2 := pri.String(), pri2.String(); b1 != b2 {
|
||||
t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
|
||||
}
|
||||
if pub1, pub2 := pri.Public().String(), pri2.Public().String(); pub1 != pub2 {
|
||||
t.Fatalf("base64-encoded public keys do not match: %s, %s", pub1, pub2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON incompatible with Key", func(t *testing.T) {
|
||||
k2 := new(Key)
|
||||
if err := k2.UnmarshalJSON(b); err == nil {
|
||||
t.Fatalf("successfully decoded private key as key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second key", func(t *testing.T) {
|
||||
// A second call to New should make a new key.
|
||||
pri3, err := NewPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(pri[:], pri3[:]) {
|
||||
t.Fatalf("pri %v == pri3 %v", pri[:], pri3[:])
|
||||
}
|
||||
// Check for obvious comparables to make sure we are not generating bad strings somewhere.
|
||||
if b1, b2 := pri.String(), pri3.String(); b1 == b2 {
|
||||
t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
|
||||
}
|
||||
if pub1, pub2 := pri.Public().String(), pri3.Public().String(); pub1 == pub2 {
|
||||
t.Fatalf("base64-encoded public keys match: %s, %s", pub1, pub2)
|
||||
}
|
||||
})
|
||||
}
|
||||
201
wgengine/wgcfg/parser.go
Normal file
201
wgengine/wgcfg/parser.go
Normal file
@@ -0,0 +1,201 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
type ParseError struct {
|
||||
why string
|
||||
offender string
|
||||
}
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
return fmt.Sprintf("%s: %q", e.why, e.offender)
|
||||
}
|
||||
|
||||
func validateEndpoints(s string) error {
|
||||
if s == "" {
|
||||
// Otherwise strings.Split of the empty string produces [""].
|
||||
return nil
|
||||
}
|
||||
vals := strings.Split(s, ",")
|
||||
for _, val := range vals {
|
||||
_, _, err := parseEndpoint(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseEndpoint(s string) (host string, port uint16, err error) {
|
||||
i := strings.LastIndexByte(s, ':')
|
||||
if i < 0 {
|
||||
return "", 0, &ParseError{"Missing port from endpoint", s}
|
||||
}
|
||||
host, portStr := s[:i], s[i+1:]
|
||||
if len(host) < 1 {
|
||||
return "", 0, &ParseError{"Invalid endpoint host", host}
|
||||
}
|
||||
uport, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
hostColon := strings.IndexByte(host, ':')
|
||||
if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 {
|
||||
err := &ParseError{"Brackets must contain an IPv6 address", host}
|
||||
if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 {
|
||||
maybeV6 := net.ParseIP(host[1 : len(host)-1])
|
||||
if maybeV6 == nil || len(maybeV6) != net.IPv6len {
|
||||
return "", 0, err
|
||||
}
|
||||
} else {
|
||||
return "", 0, err
|
||||
}
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
return host, uint16(uport), nil
|
||||
}
|
||||
|
||||
func parseKeyHex(s string) (*Key, error) {
|
||||
k, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, &ParseError{"Invalid key: " + err.Error(), s}
|
||||
}
|
||||
if len(k) != KeySize {
|
||||
return nil, &ParseError{"Keys must decode to exactly 32 bytes", s}
|
||||
}
|
||||
var key Key
|
||||
copy(key[:], k)
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
// FromUAPI generates a Config from r.
|
||||
// r should be generated by calling device.IpcGetOperation;
|
||||
// it is not compatible with other uapi streams.
|
||||
func FromUAPI(r io.Reader) (*Config, error) {
|
||||
cfg := new(Config)
|
||||
var peer *Peer // current peer being operated on
|
||||
deviceConfig := true
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(line, "=")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("failed to parse line %q, found %d =-separated parts, want 2", line, len(parts))
|
||||
}
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
if key == "public_key" {
|
||||
if deviceConfig {
|
||||
deviceConfig = false
|
||||
}
|
||||
// Load/create the peer we are now configuring.
|
||||
var err error
|
||||
peer, err = cfg.handlePublicKeyLine(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
if deviceConfig {
|
||||
err = cfg.handleDeviceLine(key, value)
|
||||
} else {
|
||||
err = cfg.handlePeerLine(peer, key, value)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handleDeviceLine(key, value string) error {
|
||||
switch key {
|
||||
case "private_key":
|
||||
k, err := parseKeyHex(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// wireguard-go guarantees not to send zero value; private keys are already clamped.
|
||||
cfg.PrivateKey = PrivateKey(*k)
|
||||
case "listen_port":
|
||||
port, err := strconv.ParseUint(value, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse listen_port: %w", err)
|
||||
}
|
||||
cfg.ListenPort = uint16(port)
|
||||
case "fwmark":
|
||||
// ignore
|
||||
default:
|
||||
return fmt.Errorf("unexpected IpcGetOperation key: %v", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handlePublicKeyLine(value string) (*Peer, error) {
|
||||
k, err := parseKeyHex(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.Peers = append(cfg.Peers, Peer{})
|
||||
peer := &cfg.Peers[len(cfg.Peers)-1]
|
||||
peer.PublicKey = *k
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handlePeerLine(peer *Peer, key, value string) error {
|
||||
switch key {
|
||||
case "endpoint":
|
||||
err := validateEndpoints(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.Endpoints = value
|
||||
case "persistent_keepalive_interval":
|
||||
n, err := strconv.ParseUint(value, 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.PersistentKeepalive = uint16(n)
|
||||
case "allowed_ip":
|
||||
ipp, err := netaddr.ParseIPPrefix(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.AllowedIPs = append(peer.AllowedIPs, ipp)
|
||||
case "protocol_version":
|
||||
if value != "1" {
|
||||
return fmt.Errorf("invalid protocol version: %v", value)
|
||||
}
|
||||
case "preshared_key", "last_handshake_time_sec", "last_handshake_time_nsec", "tx_bytes", "rx_bytes":
|
||||
// ignore
|
||||
default:
|
||||
return fmt.Errorf("unexpected IpcGetOperation key: %v", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
73
wgengine/wgcfg/parser_test.go
Normal file
73
wgengine/wgcfg/parser_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func noError(t *testing.T, err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
t.Errorf("Error at %s:%d: %#v", fn, line, err)
|
||||
return false
|
||||
}
|
||||
|
||||
func equal(t *testing.T, expected, actual interface{}) bool {
|
||||
if reflect.DeepEqual(expected, actual) {
|
||||
return true
|
||||
}
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
t.Errorf("Failed equals at %s:%d\nactual %#v\nexpected %#v", fn, line, actual, expected)
|
||||
return false
|
||||
}
|
||||
|
||||
func TestParseEndpoint(t *testing.T) {
|
||||
_, _, err := parseEndpoint("[192.168.42.0:]:51880")
|
||||
if err == nil {
|
||||
t.Error("Error was expected")
|
||||
}
|
||||
host, port, err := parseEndpoint("192.168.42.0:51880")
|
||||
if noError(t, err) {
|
||||
equal(t, "192.168.42.0", host)
|
||||
equal(t, uint16(51880), port)
|
||||
}
|
||||
host, port, err = parseEndpoint("test.wireguard.com:18981")
|
||||
if noError(t, err) {
|
||||
equal(t, "test.wireguard.com", host)
|
||||
equal(t, uint16(18981), port)
|
||||
}
|
||||
host, port, err = parseEndpoint("[2607:5300:60:6b0::c05f:543]:2468")
|
||||
if noError(t, err) {
|
||||
equal(t, "2607:5300:60:6b0::c05f:543", host)
|
||||
equal(t, uint16(2468), port)
|
||||
}
|
||||
_, _, err = parseEndpoint("[::::::invalid:18981")
|
||||
if err == nil {
|
||||
t.Error("Error was expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEndpoints(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want error
|
||||
}{
|
||||
{"", nil},
|
||||
{"1.2.3.4:5", nil},
|
||||
{"1.2.3.4:5,6.7.8.9:10", nil},
|
||||
{",", &ParseError{why: "Missing port from endpoint", offender: ""}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := validateEndpoints(tt.in)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q = %#v (%s); want %#v (%s)", tt.in, got, got, tt.want, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
141
wgengine/wgcfg/writer.go
Normal file
141
wgengine/wgcfg/writer.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// ToUAPI writes cfg in UAPI format to w.
|
||||
// Prev is the previous device Config.
|
||||
// Prev is required so that we can remove now-defunct peers
|
||||
// without having to remove and re-add all peers.
|
||||
func (cfg *Config) ToUAPI(w io.Writer, prev *Config) error {
|
||||
var stickyErr error
|
||||
set := func(key, value string) {
|
||||
if stickyErr != nil {
|
||||
return
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s=%s\n", key, value)
|
||||
if err != nil {
|
||||
stickyErr = err
|
||||
}
|
||||
}
|
||||
setUint16 := func(key string, value uint16) {
|
||||
set(key, strconv.FormatUint(uint64(value), 10))
|
||||
}
|
||||
setPeer := func(peer Peer) {
|
||||
set("public_key", peer.PublicKey.HexString())
|
||||
}
|
||||
|
||||
// Device config.
|
||||
if prev.PrivateKey != cfg.PrivateKey {
|
||||
set("private_key", cfg.PrivateKey.HexString())
|
||||
}
|
||||
if prev.ListenPort != cfg.ListenPort {
|
||||
setUint16("listen_port", cfg.ListenPort)
|
||||
}
|
||||
|
||||
old := make(map[Key]Peer)
|
||||
for _, p := range prev.Peers {
|
||||
old[p.PublicKey] = p
|
||||
}
|
||||
|
||||
// Add/configure all new peers.
|
||||
for _, p := range cfg.Peers {
|
||||
oldPeer := old[p.PublicKey]
|
||||
setPeer(p)
|
||||
set("protocol_version", "1")
|
||||
|
||||
if !endpointsEqual(oldPeer.Endpoints, p.Endpoints) {
|
||||
set("endpoint", p.Endpoints)
|
||||
}
|
||||
|
||||
// TODO: replace_allowed_ips is expensive.
|
||||
// If p.AllowedIPs is a strict superset of oldPeer.AllowedIPs,
|
||||
// then skip replace_allowed_ips and instead add only
|
||||
// the new ipps with allowed_ip.
|
||||
if !cidrsEqual(oldPeer.AllowedIPs, p.AllowedIPs) {
|
||||
set("replace_allowed_ips", "true")
|
||||
for _, ipp := range p.AllowedIPs {
|
||||
set("allowed_ip", ipp.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Set PersistentKeepalive after the peer is otherwise configured,
|
||||
// because it can trigger handshake packets.
|
||||
if oldPeer.PersistentKeepalive != p.PersistentKeepalive {
|
||||
setUint16("persistent_keepalive_interval", p.PersistentKeepalive)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove peers that were present but should no longer be.
|
||||
for _, p := range cfg.Peers {
|
||||
delete(old, p.PublicKey)
|
||||
}
|
||||
for _, p := range old {
|
||||
setPeer(p)
|
||||
set("remove", "true")
|
||||
}
|
||||
|
||||
if stickyErr != nil {
|
||||
stickyErr = fmt.Errorf("ToUAPI: %w", stickyErr)
|
||||
}
|
||||
return stickyErr
|
||||
}
|
||||
|
||||
func endpointsEqual(x, y string) bool {
|
||||
// Cheap comparisons.
|
||||
if x == y {
|
||||
return true
|
||||
}
|
||||
xs := strings.Split(x, ",")
|
||||
ys := strings.Split(y, ",")
|
||||
if len(xs) != len(ys) {
|
||||
return false
|
||||
}
|
||||
// Otherwise, see if they're the same, but out of order.
|
||||
sort.Strings(xs)
|
||||
sort.Strings(ys)
|
||||
x = strings.Join(xs, ",")
|
||||
y = strings.Join(ys, ",")
|
||||
return x == y
|
||||
}
|
||||
|
||||
func cidrsEqual(x, y []netaddr.IPPrefix) bool {
|
||||
// TODO: re-implement using netaddr.IPSet.Equal.
|
||||
if len(x) != len(y) {
|
||||
return false
|
||||
}
|
||||
// First see if they're equal in order, without allocating.
|
||||
exact := true
|
||||
for i := range x {
|
||||
if x[i] != y[i] {
|
||||
exact = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if exact {
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, see if they're the same, but out of order.
|
||||
m := make(map[netaddr.IPPrefix]bool)
|
||||
for _, v := range x {
|
||||
m[v] = true
|
||||
}
|
||||
for _, v := range y {
|
||||
if !m[v] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
// ByteCount is the number of bytes that have been sent or received.
|
||||
|
||||
89
wgengine/wglog/wglog.go
Normal file
89
wgengine/wglog/wglog.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package wglog contains logging helpers for wireguard-go.
|
||||
package wglog
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
// A Logger is a wireguard-go log wrapper that cleans up and rewrites log lines.
|
||||
// It can be modified at run time to adjust to new wireguard-go configurations.
|
||||
type Logger struct {
|
||||
DeviceLogger *device.Logger
|
||||
replacer atomic.Value // of *strings.Replacer
|
||||
}
|
||||
|
||||
// NewLogger creates a new logger for use with wireguard-go.
|
||||
// This logger silences repetitive/unhelpful noisy log lines
|
||||
// and rewrites peer keys from wireguard-go into Tailscale format.
|
||||
func NewLogger(logf logger.Logf) *Logger {
|
||||
ret := new(Logger)
|
||||
|
||||
wrapper := func(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if strings.Contains(msg, "Routine:") {
|
||||
// wireguard-go logs as it starts and stops routines.
|
||||
// Drop those; there are a lot of them, and they're just noise.
|
||||
return
|
||||
}
|
||||
r := ret.replacer.Load()
|
||||
if r == nil {
|
||||
// No replacements specified; log as originally planned.
|
||||
logf(format, args...)
|
||||
return
|
||||
}
|
||||
// Do the replacements.
|
||||
new := r.(*strings.Replacer).Replace(msg)
|
||||
if new == msg {
|
||||
// No replacements. Log as originally planned.
|
||||
logf(format, args...)
|
||||
return
|
||||
}
|
||||
// We made some replacements. Log the new version.
|
||||
// This changes the format string,
|
||||
// which is somewhat unfortunate as it impacts rate limiting,
|
||||
// but there's not much we can do about that.
|
||||
logf("%s", new)
|
||||
}
|
||||
std := logger.StdLogger(wrapper)
|
||||
ret.DeviceLogger = &device.Logger{
|
||||
Debug: std,
|
||||
Info: std,
|
||||
Error: std,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// SetPeers adjusts x to rewrite the peer public keys found in peers.
|
||||
// SetPeers is safe for concurrent use.
|
||||
func (x *Logger) SetPeers(peers []wgcfg.Peer) {
|
||||
// Construct a new peer public key log rewriter.
|
||||
var replace []string
|
||||
for _, peer := range peers {
|
||||
old := "peer(" + wireguardGoString(peer.PublicKey) + ")"
|
||||
new := peer.PublicKey.ShortString()
|
||||
replace = append(replace, old, new)
|
||||
}
|
||||
r := strings.NewReplacer(replace...)
|
||||
x.replacer.Store(r)
|
||||
}
|
||||
|
||||
// wireguardGoString prints p in the same format used by wireguard-go.
|
||||
func wireguardGoString(k wgcfg.Key) string {
|
||||
base64Key := base64.StdEncoding.EncodeToString(k[:])
|
||||
abbreviatedKey := "invalid"
|
||||
if len(base64Key) == 44 {
|
||||
abbreviatedKey = base64Key[0:4] + "…" + base64Key[39:43]
|
||||
}
|
||||
return abbreviatedKey
|
||||
}
|
||||
59
wgengine/wglog/wglog_test.go
Normal file
59
wgengine/wglog/wglog_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wglog_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wglog"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
omit bool
|
||||
}{
|
||||
{"hi", "hi", false},
|
||||
{"Routine: starting", "", true},
|
||||
{"peer(IMTB…r7lM) says it misses you", "[IMTBr] says it misses you", false},
|
||||
}
|
||||
|
||||
c := make(chan string, 1)
|
||||
logf := func(format string, args ...interface{}) {
|
||||
s := fmt.Sprintf(format, args...)
|
||||
select {
|
||||
case c <- s:
|
||||
default:
|
||||
t.Errorf("wrote %q, but shouldn't have", s)
|
||||
}
|
||||
}
|
||||
|
||||
x := wglog.NewLogger(logf)
|
||||
key, err := wgcfg.ParseHexKey("20c4c1ae54e1fd37cab6e9a532ca20646aff496796cc41d4519560e5e82bee53")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
x.SetPeers([]wgcfg.Peer{{PublicKey: key}})
|
||||
|
||||
for _, tt := range tests {
|
||||
if tt.omit {
|
||||
// Write a message ourselves into the channel.
|
||||
// Then if logf also attempts to write into the channel, it'll fail.
|
||||
c <- ""
|
||||
}
|
||||
x.DeviceLogger.Info.Println(tt.in)
|
||||
got := <-c
|
||||
if tt.omit {
|
||||
continue
|
||||
}
|
||||
tt.want += "\n"
|
||||
if got != tt.want {
|
||||
t.Errorf("Println(%q) = %q want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user