Compare commits
93 Commits
onebinary
...
simenghe/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f9c808ea1 | ||
|
|
8b11937eaf | ||
|
|
fc5fba0fbf | ||
|
|
796e222901 | ||
|
|
f0121468f4 | ||
|
|
6956645ec8 | ||
|
|
b402e76185 | ||
|
|
622dc7b093 | ||
|
|
3f1405fa2a | ||
|
|
e29cec759a | ||
|
|
8236464252 | ||
|
|
1c6946f971 | ||
|
|
7fab244614 | ||
|
|
0141390365 | ||
|
|
dfb1385fcc | ||
|
|
e92fd19484 | ||
|
|
adaecd83c8 | ||
|
|
607b7ab692 | ||
|
|
df8a5d09c3 | ||
|
|
6ce77b8eca | ||
|
|
58cc2cc921 | ||
|
|
aa6abc98f3 | ||
|
|
a573779c5c | ||
|
|
5bf65c580d | ||
|
|
ecfb2639cc | ||
|
|
713c5c9ab1 | ||
|
|
0a655309c6 | ||
|
|
a282819026 | ||
|
|
4da5e79c39 | ||
|
|
95e296fd96 | ||
|
|
5088af68cf | ||
|
|
a321c24667 | ||
|
|
9794be375d | ||
|
|
ca96357d4b | ||
|
|
33bc06795b | ||
|
|
c54cc24e87 | ||
|
|
d7f6ef3a79 | ||
|
|
caaefa00a0 | ||
|
|
2802a01b81 | ||
|
|
eaa6507cc9 | ||
|
|
8a7d35594d | ||
|
|
36cb69002a | ||
|
|
e1b994f7ed | ||
|
|
fa548c5b96 | ||
|
|
14c1113d2b | ||
|
|
ca455ac84b | ||
|
|
f21982f854 | ||
|
|
ddf6c8c729 | ||
|
|
4cfaf489ac | ||
|
|
6d6cf88d82 | ||
|
|
1f72b6f812 | ||
|
|
35749ec297 | ||
|
|
a04801e037 | ||
|
|
82b217f82e | ||
|
|
50c976d3f1 | ||
|
|
d2c4e75099 | ||
|
|
cdd231cb7d | ||
|
|
ba59c0391b | ||
|
|
60e920bf18 | ||
|
|
bb8ce48a6b | ||
|
|
1ece91cede | ||
|
|
ceaaa23962 | ||
|
|
c065cc6169 | ||
|
|
4b51fbf48c | ||
|
|
e66d4e4c81 | ||
|
|
b340beff8e | ||
|
|
15a7ff83de | ||
|
|
051d2f47e5 | ||
|
|
c06ec45f09 | ||
|
|
adfe8cf41d | ||
|
|
73adbb7a78 | ||
|
|
ce7a87e5e4 | ||
|
|
135b641332 | ||
|
|
988dfcabef | ||
|
|
b371588ce6 | ||
|
|
09afb8e35b | ||
|
|
a2d7a2aeb1 | ||
|
|
020e904f4e | ||
|
|
bbb79f2d6a | ||
|
|
79b7fa9ac3 | ||
|
|
a86a0361a7 | ||
|
|
8bf2a38f29 | ||
|
|
5666663370 | ||
|
|
d6d1951897 | ||
|
|
df350e2069 | ||
|
|
eb9757a290 | ||
|
|
cd54792fe9 | ||
|
|
293a2b11cd | ||
|
|
e2dcf63420 | ||
|
|
6690f86ef4 | ||
|
|
dd0b690e7b | ||
|
|
85df1b0fa7 | ||
|
|
234cc87f48 |
36
.github/workflows/xe-experimental-vm-test.yml
vendored
Normal file
36
.github/workflows/xe-experimental-vm-test.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: "integration-vms"
|
||||
|
||||
on:
|
||||
# # NOTE(Xe): uncomment this region when testing the test
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - 'main'
|
||||
release:
|
||||
types: [ created ]
|
||||
schedule:
|
||||
# At minute 0 past hour 6 and 18
|
||||
# https://crontab.guru/#00_6,18_*_*_*
|
||||
- cron: '00 6,18 * * *'
|
||||
|
||||
jobs:
|
||||
experimental-linux-vm-test:
|
||||
# To set up a new runner, see tstest/integration/vms/runner.nix
|
||||
runs-on: [ self-hosted, linux, vm_integration_test ]
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download VM Images
|
||||
run: go test ./tstest/integration/vms -run-vm-tests -run=Download -timeout=60m
|
||||
env:
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
- name: Run VM tests
|
||||
run: go test ./tstest/integration/vms -v -run-vm-tests
|
||||
env:
|
||||
TMPDIR: "/tmp"
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
@@ -11,6 +11,36 @@
|
||||
|
||||
set -eu
|
||||
|
||||
eval $(./version/version.sh)
|
||||
IFS=".$IFS" read -r major minor patch <VERSION.txt
|
||||
git_hash=$(git rev-parse HEAD)
|
||||
if ! git diff-index --quiet HEAD; then
|
||||
git_hash="${git_hash}-dirty"
|
||||
fi
|
||||
base_hash=$(git rev-list --max-count=1 HEAD -- VERSION.txt)
|
||||
change_count=$(git rev-list --count HEAD "^$base_hash")
|
||||
short_hash=$(echo "$git_hash" | cut -c1-9)
|
||||
|
||||
exec go build -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}" "$@"
|
||||
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
|
||||
patch="$change_count"
|
||||
change_suffix=""
|
||||
elif [ "$change_count" != "0" ]; then
|
||||
change_suffix="-$change_count"
|
||||
else
|
||||
change_suffix=""
|
||||
fi
|
||||
|
||||
long_suffix="$change_suffix-t$short_hash"
|
||||
SHORT="$major.$minor.$patch"
|
||||
LONG="${SHORT}$long_suffix"
|
||||
GIT_HASH="$git_hash"
|
||||
|
||||
if [ "$1" = "shellvars" ]; then
|
||||
cat <<EOF
|
||||
VERSION_SHORT="$SHORT"
|
||||
VERSION_LONG="$LONG"
|
||||
VERSION_GIT_HASH="$GIT_HASH"
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"
|
||||
|
||||
@@ -256,3 +256,25 @@ func Logout(ctx context.Context) error {
|
||||
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetDNS adds a DNS TXT record for the given domain name, containing
|
||||
// the provided TXT value. The intended use case is answering
|
||||
// LetsEncrypt/ACME dns-01 challenges.
|
||||
//
|
||||
// The control plane will only permit SetDNS requests with very
|
||||
// specific names and values. The name should be
|
||||
// "_acme-challenge." + your node's MagicDNS name. It's expected that
|
||||
// clients cache the certs from LetsEncrypt (or whichever CA is
|
||||
// providing them) and only request new ones as needed; the control plane
|
||||
// rate limits SetDNS requests.
|
||||
//
|
||||
// This is a low-level interface; it's expected that most Tailscale
|
||||
// users use a higher level interface to getting/using TLS
|
||||
// certificates.
|
||||
func SetDNS(ctx context.Context, name, value string) error {
|
||||
v := url.Values{}
|
||||
v.Set("name", name)
|
||||
v.Set("value", value)
|
||||
_, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ import (
|
||||
// into a map of filePathOnDisk -> filePathInPackage.
|
||||
func parseFiles(s string) (map[string]string, error) {
|
||||
ret := map[string]string{}
|
||||
if len(s) == 0 {
|
||||
return ret, nil
|
||||
}
|
||||
for _, f := range strings.Split(s, ",") {
|
||||
fs := strings.Split(f, ":")
|
||||
if len(fs) != 2 {
|
||||
|
||||
@@ -230,7 +230,9 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
if defaultNetfilterMode() != "off" {
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid value --netfilter-mode=%q", upArgs.netfilterMode)
|
||||
}
|
||||
@@ -266,7 +268,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
|
||||
notSupported := "not supported on Synology; see https://github.com/tailscale/tailscale/issues/1995"
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
@@ -15,11 +16,13 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -82,17 +85,63 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
}
|
||||
|
||||
func auth() (string, error) {
|
||||
// authorize checks whether the provided user has access to the web UI.
|
||||
func authorize(name string) error {
|
||||
if distro.Get() == distro.Synology {
|
||||
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth: %v: %s", err, out)
|
||||
}
|
||||
return string(out), nil
|
||||
return authorizeSynology(name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
// authorizeSynology checks whether the provided user has access to the web UI
|
||||
// by consulting the membership of the "administrators" group.
|
||||
func authorizeSynology(name string) error {
|
||||
f, err := os.Open("/etc/group")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
s := bufio.NewScanner(f)
|
||||
var agLine string
|
||||
for s.Scan() {
|
||||
if !mem.HasPrefix(mem.B(s.Bytes()), mem.S("administrators:")) {
|
||||
continue
|
||||
}
|
||||
agLine = s.Text()
|
||||
break
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if agLine == "" {
|
||||
return fmt.Errorf("admin group not defined")
|
||||
}
|
||||
agEntry := strings.Split(agLine, ":")
|
||||
if len(agEntry) < 4 {
|
||||
return fmt.Errorf("malformed admin group entry")
|
||||
}
|
||||
agMembers := agEntry[3]
|
||||
for _, m := range strings.Split(agMembers, ",") {
|
||||
if m == name {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("not a member of administrators group")
|
||||
}
|
||||
|
||||
// authenticate returns the name of the user accessing the web UI.
|
||||
// Note: This is different from a tailscale user, and is typically the local
|
||||
// user on the node.
|
||||
func authenticate() (string, error) {
|
||||
if distro.Get() != distro.Synology {
|
||||
return "", nil
|
||||
}
|
||||
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth: %v: %s", err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
||||
@@ -198,8 +247,13 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth()
|
||||
user, err := authenticate()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := authorize(user); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -214,7 +268,8 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUpForceReauth(r.Context())
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(mi{"error": err})
|
||||
w.WriteHeader(500)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(mi{"url": url})
|
||||
@@ -320,6 +375,10 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error)
|
||||
})
|
||||
bc.StartLoginInteractive()
|
||||
|
||||
<-pumpCtx.Done() // wait for authURL or complete failure
|
||||
if authURL == "" && retErr == nil {
|
||||
retErr = pumpCtx.Err()
|
||||
}
|
||||
if authURL == "" && retErr == nil {
|
||||
return "", fmt.Errorf("login failed with no backend error message")
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
@@ -48,14 +49,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
L tailscale.com/util/lineread from tailscale.com/net/interfaces
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// The tailscale command is the Tailscale command-line client. It interacts
|
||||
// with the tailscaled node agent.
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
package main // import "tailscale.com/cmd/tailscale"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/cmd/tailscaled/cli"
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
)
|
||||
|
||||
func tailscale_main() {
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
@@ -1,41 +1,43 @@
|
||||
tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
W 💣 github.com/github/certstore from tailscale.com/control/controlclient
|
||||
github.com/go-multierror/multierror from tailscale.com/wgengine/router+
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/snappy from github.com/klauspost/compress/zstd
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/snappy from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink from tailscale.com/wgengine/monitor+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/mdlayher/netlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
W github.com/pkg/errors from github.com/github/certstore
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
W github.com/pkg/errors from github.com/tailscale/certstore
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
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+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
|
||||
💣 golang.zx2c4.com/wireguard/device from tailscale.com/net/tstun+
|
||||
💣 golang.zx2c4.com/wireguard/ipc from golang.zx2c4.com/wireguard/device
|
||||
W 💣 golang.zx2c4.com/wireguard/ipc/winpipe from golang.zx2c4.com/wireguard/ipc
|
||||
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
|
||||
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device+
|
||||
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/tun/wintun from golang.zx2c4.com/wireguard/tun+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
|
||||
@@ -69,6 +71,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netstack/tcpip/transport/udp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
@@ -115,7 +118,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
W 💣 tailscale.com/tempfork/wireguard-windows/firewall from tailscale.com/cmd/tailscaled
|
||||
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+
|
||||
@@ -128,7 +130,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
||||
@@ -143,6 +144,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/winutil from tailscale.com/logpolicy+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
|
||||
@@ -154,7 +156,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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+
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
@@ -163,7 +165,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
@@ -171,15 +173,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+
|
||||
LD golang.org/x/sys/unix from github.com/mdlayher/netlink+
|
||||
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if strings.HasSuffix(os.Args[0], "tailscaled") {
|
||||
tailscaled_main()
|
||||
} else if strings.HasSuffix(os.Args[0], "tailscale") {
|
||||
tailscale_main()
|
||||
} else {
|
||||
panic(os.Args[0])
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ var subCommands = map[string]*func([]string) error{
|
||||
"debug": &debugModeFunc,
|
||||
}
|
||||
|
||||
func tailscaled_main() {
|
||||
func main() {
|
||||
// We aren't very performance sensitive, and the parts that are
|
||||
// performance sensitive (wireguard) try hard not to do any memory
|
||||
// allocations. So let's be aggressive about garbage collection,
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -32,9 +31,9 @@ import (
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tempfork/wireguard-windows/firewall"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -144,13 +143,13 @@ func beFirewallKillswitch() bool {
|
||||
|
||||
luid, err := winipcfg.LUIDFromGUID(&guid)
|
||||
if err != nil {
|
||||
log.Fatalf("no interface with GUID %q", guid)
|
||||
log.Fatalf("no interface with GUID %q: %v", guid, err)
|
||||
}
|
||||
|
||||
noProtection := false
|
||||
var dnsIPs []net.IP // unused in called code.
|
||||
start := time.Now()
|
||||
firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs)
|
||||
if _, err := wf.New(uint64(luid)); err != nil {
|
||||
log.Fatalf("filewall creation failed: %v", err)
|
||||
}
|
||||
log.Printf("killswitch enabled, took %s", time.Since(start))
|
||||
|
||||
// Block until the monitor goroutine shuts us down.
|
||||
|
||||
@@ -576,9 +576,12 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
|
||||
var p *persist.Persist
|
||||
var fin *empty.Message
|
||||
var loginFin, logoutFin *empty.Message
|
||||
if state == StateAuthenticated {
|
||||
fin = new(empty.Message)
|
||||
loginFin = new(empty.Message)
|
||||
}
|
||||
if state == StateNotAuthenticated {
|
||||
logoutFin = new(empty.Message)
|
||||
}
|
||||
if nm != nil && loggedIn && synced {
|
||||
pp := c.direct.GetPersist()
|
||||
@@ -589,12 +592,13 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
nm = nil
|
||||
}
|
||||
new := Status{
|
||||
LoginFinished: fin,
|
||||
URL: url,
|
||||
Persist: p,
|
||||
NetMap: nm,
|
||||
Hostinfo: hi,
|
||||
State: state,
|
||||
LoginFinished: loginFin,
|
||||
LogoutFinished: logoutFin,
|
||||
URL: url,
|
||||
Persist: p,
|
||||
NetMap: nm,
|
||||
Hostinfo: hi,
|
||||
State: state,
|
||||
}
|
||||
if err != nil {
|
||||
new.Err = err.Error()
|
||||
@@ -712,3 +716,9 @@ func (c *Auto) TestOnlySetAuthKey(authkey string) {
|
||||
func (c *Auto) TestOnlyTimeNow() time.Time {
|
||||
return c.timeNow()
|
||||
}
|
||||
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
return c.direct.SetDNS(ctx, req)
|
||||
}
|
||||
|
||||
@@ -74,4 +74,7 @@ type Client interface {
|
||||
// in a separate http request. It has nothing to do with the rest of
|
||||
// the state machine.
|
||||
UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint)
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
|
||||
func TestStatusEqual(t *testing.T) {
|
||||
// Verify that the Equal method stays in sync with reality
|
||||
equalHandles := []string{"LoginFinished", "Err", "URL", "NetMap", "State", "Persist", "Hostinfo"}
|
||||
equalHandles := []string{"LoginFinished", "LogoutFinished", "Err", "URL", "NetMap", "State", "Persist", "Hostinfo"}
|
||||
if have := fieldsOf(reflect.TypeOf(Status{})); !reflect.DeepEqual(have, equalHandles) {
|
||||
t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, equalHandles)
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
@@ -66,6 +67,7 @@ type Direct struct {
|
||||
debugFlags []string
|
||||
keepSharerAndUserSplit bool
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgkey.Key
|
||||
@@ -103,6 +105,18 @@ type Options struct {
|
||||
// forwarding works and should not be double-checked by the
|
||||
// controlclient package.
|
||||
SkipIPForwardingCheck bool
|
||||
|
||||
// Pinger optionally specifies the Pinger to use to satisfy
|
||||
// MapResponse.PingRequest queries from the control plane.
|
||||
// If nil, PingRequest queries are not answered.
|
||||
Pinger Pinger
|
||||
}
|
||||
|
||||
// Pinger is a subset of the wgengine.Engine interface, containing just the Ping method.
|
||||
type Pinger interface {
|
||||
// Ping is a request to start a discovery or TSMP ping with the peer handling
|
||||
// the given IP and then call cb with its ping latency & method.
|
||||
Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult))
|
||||
}
|
||||
|
||||
type Decompressor interface {
|
||||
@@ -165,6 +179,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
|
||||
linkMon: opts.LinkMonitor,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
@@ -1211,3 +1226,50 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<-
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
c.mu.Unlock()
|
||||
|
||||
if serverKey.IsZero() {
|
||||
return errors.New("zero serverKey")
|
||||
}
|
||||
machinePrivKey, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getMachinePrivKey: %w", err)
|
||||
}
|
||||
if machinePrivKey.IsZero() {
|
||||
return errors.New("getMachinePrivKey returned zero key")
|
||||
}
|
||||
|
||||
bodyData, err := encode(req, &serverKey, &machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := bytes.NewReader(bodyData)
|
||||
|
||||
u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().HexString())
|
||||
hreq, err := http.NewRequestWithContext(ctx, "POST", u, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := c.httpc.Do(hreq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
return fmt.Errorf("sign-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
var setDNSRes struct{} // no fields yet
|
||||
if err := decode(res, &setDNSRes, &serverKey, &machinePrivKey); err != nil {
|
||||
c.logf("error decoding SetDNSResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return fmt.Errorf("set-dns-response: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/github/certstore"
|
||||
"github.com/tailscale/certstore"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/winutil"
|
||||
|
||||
@@ -64,11 +64,12 @@ func (s State) String() string {
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message // nonempty when login finishes
|
||||
Err string
|
||||
URL string // interactive URL to visit to finish logging in
|
||||
NetMap *netmap.NetworkMap // server-pushed configuration
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message // nonempty when login finishes
|
||||
LogoutFinished *empty.Message // nonempty when logout finishes
|
||||
Err string
|
||||
URL string // interactive URL to visit to finish logging in
|
||||
NetMap *netmap.NetworkMap // server-pushed configuration
|
||||
|
||||
// The internal state should not be exposed outside this
|
||||
// package, but we have some automated tests elsewhere that need to
|
||||
@@ -86,6 +87,7 @@ func (s *Status) Equal(s2 *Status) bool {
|
||||
}
|
||||
return s != nil && s2 != nil &&
|
||||
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
|
||||
(s.LogoutFinished == nil) == (s2.LogoutFinished == nil) &&
|
||||
s.Err == s2.Err &&
|
||||
s.URL == s2.URL &&
|
||||
reflect.DeepEqual(s.Persist, s2.Persist) &&
|
||||
|
||||
61
go.mod
61
go.mod
@@ -3,48 +3,47 @@ module tailscale.com
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
|
||||
github.com/coreos/go-iptables v0.4.5
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/frankban/quicktest v1.12.1
|
||||
github.com/github/certstore v0.1.0
|
||||
github.com/gliderlabs/ssh v0.2.2
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aws/aws-sdk-go v1.38.52
|
||||
github.com/coreos/go-iptables v0.6.0
|
||||
github.com/frankban/quicktest v1.13.0
|
||||
github.com/gliderlabs/ssh v0.3.2
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/google/go-cmp v0.5.5
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b
|
||||
github.com/go-ole/go-ole v1.2.5
|
||||
github.com/godbus/dbus/v5 v5.0.4
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/klauspost/compress v1.12.2
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mdlayher/netlink v1.3.2
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/mdlayher/netlink v1.4.1
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
|
||||
github.com/miekg/dns v1.1.42
|
||||
github.com/pborman/getopt v1.1.0
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/sftp v1.13.0
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
golang.org/x/tools v0.1.0
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841
|
||||
golang.org/x/tools v0.1.2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5
|
||||
golang.zx2c4.com/wireguard/windows v0.3.15-0.20210525143335-94c0476d63e3
|
||||
honnef.co/go/tools v0.1.4
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
replace github.com/github/certstore => github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2
|
||||
|
||||
@@ -9,26 +9,28 @@ package deephash
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"reflect"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Hash(v ...interface{}) string {
|
||||
func calcHash(v interface{}) string {
|
||||
h := sha256.New()
|
||||
// 64 matches the chunk size in crypto/sha256/sha256.go
|
||||
b := bufio.NewWriterSize(h, 64)
|
||||
Print(b, v)
|
||||
b := bufio.NewWriterSize(h, h.BlockSize())
|
||||
scratch := make([]byte, 0, 128)
|
||||
printTo(b, v, scratch)
|
||||
b.Flush()
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
scratch = h.Sum(scratch[:0])
|
||||
hex.Encode(scratch[:cap(scratch)], scratch[:sha256.Size])
|
||||
return string(scratch[:sha256.Size*2])
|
||||
}
|
||||
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
sig := calcHash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
@@ -36,81 +38,30 @@ func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w *bufio.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
func printTo(w *bufio.Writer, v interface{}, scratch []byte) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool), scratch)
|
||||
}
|
||||
|
||||
var (
|
||||
netaddrIPType = reflect.TypeOf(netaddr.IP{})
|
||||
netaddrIPPrefix = reflect.TypeOf(netaddr.IPPrefix{})
|
||||
wgkeyKeyType = reflect.TypeOf(wgkey.Key{})
|
||||
wgkeyPrivateType = reflect.TypeOf(wgkey.Private{})
|
||||
tailcfgDiscoKeyType = reflect.TypeOf(tailcfg.DiscoKey{})
|
||||
)
|
||||
var appenderToType = reflect.TypeOf((*appenderTo)(nil)).Elem()
|
||||
|
||||
func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
type appenderTo interface {
|
||||
AppendTo([]byte) []byte
|
||||
}
|
||||
|
||||
// print hashes v into w.
|
||||
// It reports whether it was able to do so without hitting a cycle.
|
||||
func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool, scratch []byte) (acyclic bool) {
|
||||
if !v.IsValid() {
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
// Special case some common types.
|
||||
if v.CanInterface() {
|
||||
switch v.Type() {
|
||||
case netaddrIPType:
|
||||
var b []byte
|
||||
var err error
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*netaddr.IP)
|
||||
b, err = x.MarshalText()
|
||||
} else {
|
||||
x := v.Interface().(netaddr.IP)
|
||||
b, err = x.MarshalText()
|
||||
}
|
||||
if err == nil {
|
||||
w.Write(b)
|
||||
return
|
||||
}
|
||||
case netaddrIPPrefix:
|
||||
var b []byte
|
||||
var err error
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*netaddr.IPPrefix)
|
||||
b, err = x.MarshalText()
|
||||
} else {
|
||||
x := v.Interface().(netaddr.IPPrefix)
|
||||
b, err = x.MarshalText()
|
||||
}
|
||||
if err == nil {
|
||||
w.Write(b)
|
||||
return
|
||||
}
|
||||
case wgkeyKeyType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*wgkey.Key)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(wgkey.Key)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
case wgkeyPrivateType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*wgkey.Private)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(wgkey.Private)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
case tailcfgDiscoKeyType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*tailcfg.DiscoKey)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(tailcfg.DiscoKey)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
// Use AppendTo methods, if available and cheap.
|
||||
if v.CanAddr() && v.Type().Implements(appenderToType) {
|
||||
a := v.Addr().Interface().(appenderTo)
|
||||
scratch = a.AppendTo(scratch[:0])
|
||||
w.Write(scratch)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,43 +72,45 @@ func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
case reflect.Ptr:
|
||||
ptr := v.Pointer()
|
||||
if visited[ptr] {
|
||||
return
|
||||
return false
|
||||
}
|
||||
visited[ptr] = true
|
||||
print(w, v.Elem(), visited)
|
||||
return
|
||||
return print(w, v.Elem(), visited, scratch)
|
||||
case reflect.Struct:
|
||||
acyclic = true
|
||||
w.WriteString("struct{\n")
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Field(i), visited)
|
||||
if !print(w, v.Field(i), visited, scratch) {
|
||||
acyclic = false
|
||||
}
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
return acyclic
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||
fmt.Fprintf(w, "%q", v.Interface())
|
||||
return
|
||||
return true
|
||||
}
|
||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
||||
acyclic = true
|
||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Index(i), visited)
|
||||
if !print(w, v.Index(i), visited, scratch) {
|
||||
acyclic = false
|
||||
}
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
return acyclic
|
||||
case reflect.Interface:
|
||||
print(w, v.Elem(), visited)
|
||||
return print(w, v.Elem(), visited, scratch)
|
||||
case reflect.Map:
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
print(w, k, visited)
|
||||
w.WriteString(": ")
|
||||
print(w, sm.Value[i], visited)
|
||||
w.WriteString("\n")
|
||||
if hashMapAcyclic(w, v, visited, scratch) {
|
||||
return true
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
return hashMapFallback(w, v, visited, scratch)
|
||||
case reflect.String:
|
||||
w.WriteString(v.String())
|
||||
case reflect.Bool:
|
||||
@@ -165,10 +118,109 @@ func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "%v", v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "%v", v.Uint())
|
||||
scratch = strconv.AppendUint(scratch[:0], v.Uint(), 10)
|
||||
w.Write(scratch)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "%v", v.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(w, "%v", v.Complex())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type mapHasher struct {
|
||||
xbuf [sha256.Size]byte // XOR'ed accumulated buffer
|
||||
ebuf [sha256.Size]byte // scratch buffer
|
||||
s256 hash.Hash // sha256 hash.Hash
|
||||
bw *bufio.Writer // to hasher into ebuf
|
||||
val valueCache // re-usable values for map iteration
|
||||
iter *reflect.MapIter // re-usable map iterator
|
||||
}
|
||||
|
||||
func (mh *mapHasher) Reset() {
|
||||
for i := range mh.xbuf {
|
||||
mh.xbuf[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *mapHasher) startEntry() {
|
||||
for i := range mh.ebuf {
|
||||
mh.ebuf[i] = 0
|
||||
}
|
||||
mh.bw.Flush()
|
||||
mh.s256.Reset()
|
||||
}
|
||||
|
||||
func (mh *mapHasher) endEntry() {
|
||||
mh.bw.Flush()
|
||||
for i, b := range mh.s256.Sum(mh.ebuf[:0]) {
|
||||
mh.xbuf[i] ^= b
|
||||
}
|
||||
}
|
||||
|
||||
var mapHasherPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
mh := new(mapHasher)
|
||||
mh.s256 = sha256.New()
|
||||
mh.bw = bufio.NewWriter(mh.s256)
|
||||
mh.val = make(valueCache)
|
||||
mh.iter = new(reflect.MapIter)
|
||||
return mh
|
||||
},
|
||||
}
|
||||
|
||||
type valueCache map[reflect.Type]reflect.Value
|
||||
|
||||
func (c valueCache) get(t reflect.Type) reflect.Value {
|
||||
v, ok := c[t]
|
||||
if !ok {
|
||||
v = reflect.New(t).Elem()
|
||||
c[t] = v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// hashMapAcyclic is the faster sort-free version of map hashing. If
|
||||
// it detects a cycle it returns false and guarantees that nothing was
|
||||
// written to w.
|
||||
func hashMapAcyclic(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool, scratch []byte) (acyclic bool) {
|
||||
mh := mapHasherPool.Get().(*mapHasher)
|
||||
defer mapHasherPool.Put(mh)
|
||||
mh.Reset()
|
||||
iter := mapIter(mh.iter, v)
|
||||
defer mapIter(mh.iter, reflect.Value{}) // avoid pinning v from mh.iter when we return
|
||||
k := mh.val.get(v.Type().Key())
|
||||
e := mh.val.get(v.Type().Elem())
|
||||
for iter.Next() {
|
||||
key := iterKey(iter, k)
|
||||
val := iterVal(iter, e)
|
||||
mh.startEntry()
|
||||
if !print(mh.bw, key, visited, scratch) {
|
||||
return false
|
||||
}
|
||||
if !print(mh.bw, val, visited, scratch) {
|
||||
return false
|
||||
}
|
||||
mh.endEntry()
|
||||
}
|
||||
w.Write(mh.xbuf[:])
|
||||
return true
|
||||
}
|
||||
|
||||
func hashMapFallback(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool, scratch []byte) (acyclic bool) {
|
||||
acyclic = true
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
if !print(w, k, visited, scratch) {
|
||||
acyclic = false
|
||||
}
|
||||
w.WriteString(": ")
|
||||
if !print(w, sm.Value[i], visited, scratch) {
|
||||
acyclic = false
|
||||
}
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
return acyclic
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
package deephash
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -14,15 +18,15 @@ import (
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestDeepPrint(t *testing.T) {
|
||||
func TestDeepHash(t *testing.T) {
|
||||
// v contains the types of values we care about for our current callers.
|
||||
// Mostly we're just testing that we don't panic on handled types.
|
||||
v := getVal()
|
||||
|
||||
hash1 := Hash(v)
|
||||
hash1 := calcHash(v)
|
||||
t.Logf("hash: %v", hash1)
|
||||
for i := 0; i < 20; i++ {
|
||||
hash2 := Hash(getVal())
|
||||
hash2 := calcHash(getVal())
|
||||
if hash1 != hash2 {
|
||||
t.Error("second hash didn't match")
|
||||
}
|
||||
@@ -51,14 +55,23 @@ func getVal() []interface{} {
|
||||
map[dnsname.FQDN][]netaddr.IP{
|
||||
dnsname.FQDN("a."): {netaddr.MustParseIP("1.2.3.4"), netaddr.MustParseIP("4.3.2.1")},
|
||||
dnsname.FQDN("b."): {netaddr.MustParseIP("8.8.8.8"), netaddr.MustParseIP("9.9.9.9")},
|
||||
dnsname.FQDN("c."): {netaddr.MustParseIP("6.6.6.6"), netaddr.MustParseIP("7.7.7.7")},
|
||||
dnsname.FQDN("d."): {netaddr.MustParseIP("6.7.6.6"), netaddr.MustParseIP("7.7.7.8")},
|
||||
dnsname.FQDN("e."): {netaddr.MustParseIP("6.8.6.6"), netaddr.MustParseIP("7.7.7.9")},
|
||||
dnsname.FQDN("f."): {netaddr.MustParseIP("6.9.6.6"), netaddr.MustParseIP("7.7.7.0")},
|
||||
},
|
||||
map[dnsname.FQDN][]netaddr.IPPort{
|
||||
dnsname.FQDN("a."): {netaddr.MustParseIPPort("1.2.3.4:11"), netaddr.MustParseIPPort("4.3.2.1:22")},
|
||||
dnsname.FQDN("b."): {netaddr.MustParseIPPort("8.8.8.8:11"), netaddr.MustParseIPPort("9.9.9.9:22")},
|
||||
dnsname.FQDN("c."): {netaddr.MustParseIPPort("8.8.8.8:12"), netaddr.MustParseIPPort("9.9.9.9:23")},
|
||||
dnsname.FQDN("d."): {netaddr.MustParseIPPort("8.8.8.8:13"), netaddr.MustParseIPPort("9.9.9.9:24")},
|
||||
dnsname.FQDN("e."): {netaddr.MustParseIPPort("8.8.8.8:14"), netaddr.MustParseIPPort("9.9.9.9:25")},
|
||||
},
|
||||
map[tailcfg.DiscoKey]bool{
|
||||
{1: 1}: true,
|
||||
{1: 2}: false,
|
||||
{2: 3}: true,
|
||||
{3: 4}: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -67,6 +80,57 @@ func BenchmarkHash(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
v := getVal()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Hash(v)
|
||||
calcHash(v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashMapAcyclic(t *testing.T) {
|
||||
m := map[int]string{}
|
||||
for i := 0; i < 100; i++ {
|
||||
m[i] = fmt.Sprint(i)
|
||||
}
|
||||
got := map[string]bool{}
|
||||
|
||||
var buf bytes.Buffer
|
||||
bw := bufio.NewWriter(&buf)
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
visited := map[uintptr]bool{}
|
||||
scratch := make([]byte, 0, 64)
|
||||
v := reflect.ValueOf(m)
|
||||
buf.Reset()
|
||||
bw.Reset(&buf)
|
||||
if !hashMapAcyclic(bw, v, visited, scratch) {
|
||||
t.Fatal("returned false")
|
||||
}
|
||||
if got[string(buf.Bytes())] {
|
||||
continue
|
||||
}
|
||||
got[string(buf.Bytes())] = true
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Errorf("got %d results; want 1", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHashMapAcyclic(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
m := map[int]string{}
|
||||
for i := 0; i < 100; i++ {
|
||||
m[i] = fmt.Sprint(i)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
bw := bufio.NewWriter(&buf)
|
||||
visited := map[uintptr]bool{}
|
||||
scratch := make([]byte, 0, 64)
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
bw.Reset(&buf)
|
||||
if !hashMapAcyclic(bw, v, visited, scratch) {
|
||||
b.Fatal("returned false")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
internal/deephash/mapiter.go
Normal file
37
internal/deephash/mapiter.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !tailscale_go
|
||||
|
||||
package deephash
|
||||
|
||||
import "reflect"
|
||||
|
||||
// iterKey returns the current iter key.
|
||||
// scratch is a re-usable reflect.Value.
|
||||
// iterKey may store the iter key in scratch and return scratch,
|
||||
// or it may allocate and return a new reflect.Value.
|
||||
func iterKey(iter *reflect.MapIter, _ reflect.Value) reflect.Value {
|
||||
return iter.Key()
|
||||
}
|
||||
|
||||
// iterVal returns the current iter val.
|
||||
// scratch is a re-usable reflect.Value.
|
||||
// iterVal may store the iter val in scratch and return scratch,
|
||||
// or it may allocate and return a new reflect.Value.
|
||||
func iterVal(iter *reflect.MapIter, _ reflect.Value) reflect.Value {
|
||||
return iter.Value()
|
||||
}
|
||||
|
||||
// mapIter returns a map iterator for mapVal.
|
||||
// scratch is a re-usable reflect.MapIter.
|
||||
// mapIter may re-use scratch and return it,
|
||||
// or it may allocate and return a new *reflect.MapIter.
|
||||
// If mapVal is the zero reflect.Value, mapIter may return nil.
|
||||
func mapIter(_ *reflect.MapIter, mapVal reflect.Value) *reflect.MapIter {
|
||||
if !mapVal.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return mapVal.MapRange()
|
||||
}
|
||||
42
internal/deephash/mapiter_future.go
Normal file
42
internal/deephash/mapiter_future.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build tailscale_go
|
||||
|
||||
package deephash
|
||||
|
||||
import "reflect"
|
||||
|
||||
// iterKey returns the current iter key.
|
||||
// scratch is a re-usable reflect.Value.
|
||||
// iterKey may store the iter key in scratch and return scratch,
|
||||
// or it may allocate and return a new reflect.Value.
|
||||
func iterKey(iter *reflect.MapIter, scratch reflect.Value) reflect.Value {
|
||||
iter.SetKey(scratch)
|
||||
return scratch
|
||||
}
|
||||
|
||||
// iterVal returns the current iter val.
|
||||
// scratch is a re-usable reflect.Value.
|
||||
// iterVal may store the iter val in scratch and return scratch,
|
||||
// or it may allocate and return a new reflect.Value.
|
||||
func iterVal(iter *reflect.MapIter, scratch reflect.Value) reflect.Value {
|
||||
iter.SetValue(scratch)
|
||||
return scratch
|
||||
}
|
||||
|
||||
// mapIter returns a map iterator for mapVal.
|
||||
// scratch is a re-usable reflect.MapIter.
|
||||
// mapIter may re-use scratch and return it,
|
||||
// or it may allocate and return a new *reflect.MapIter.
|
||||
// If mapVal is the zero reflect.Value, mapIter may return nil.
|
||||
func mapIter(scratch *reflect.MapIter, mapVal reflect.Value) *reflect.MapIter {
|
||||
scratch.Reset(mapVal) // always Reset, to allow the caller to avoid pinning memory
|
||||
if !mapVal.IsValid() {
|
||||
// Returning scratch would also be OK.
|
||||
// Do this for consistency with the non-optimized version.
|
||||
return nil
|
||||
}
|
||||
return scratch
|
||||
}
|
||||
@@ -44,11 +44,13 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -451,6 +453,13 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// Lock b once and do only the things that require locking.
|
||||
b.mu.Lock()
|
||||
|
||||
if st.LogoutFinished != nil {
|
||||
// Since we're logged out now, our netmap cache is invalid.
|
||||
// Since st.NetMap==nil means "netmap is unchanged", there is
|
||||
// no other way to represent this change.
|
||||
b.setNetMapLocked(nil)
|
||||
}
|
||||
|
||||
prefs := b.prefs
|
||||
stateKey := b.stateKey
|
||||
netMap := b.netMap
|
||||
@@ -648,6 +657,12 @@ func (b *LocalBackend) getNewControlClientFunc() clientGen {
|
||||
// startIsNoopLocked reports whether a Start call on this LocalBackend
|
||||
// with the provided Start Options would be a useless no-op.
|
||||
//
|
||||
// TODO(apenwarr): we shouldn't need this.
|
||||
// The state machine is now nearly clean enough where it can accept a new
|
||||
// connection while in any state, not just Running, and on any platform.
|
||||
// We'd want to add a few more tests to state_test.go to ensure this continues
|
||||
// to work as expected.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) startIsNoopLocked(opts ipn.Options) bool {
|
||||
// Options has 5 fields; check all of them:
|
||||
@@ -701,6 +716,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
b.send(ipn.Notify{
|
||||
State: &state,
|
||||
NetMap: nm,
|
||||
Prefs: b.prefs,
|
||||
LoginFinished: new(empty.Message),
|
||||
})
|
||||
return nil
|
||||
@@ -913,8 +929,8 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
localNets := localNetsB.IPSet()
|
||||
logNets := logNetsB.IPSet()
|
||||
localNets, _ := localNetsB.IPSet()
|
||||
logNets, _ := logNetsB.IPSet()
|
||||
|
||||
changed := deephash.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
if !changed {
|
||||
@@ -971,7 +987,8 @@ func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return b.IPSet(), hostIPs, nil
|
||||
ipSet, _ := b.IPSet()
|
||||
return ipSet, hostIPs, nil
|
||||
}
|
||||
|
||||
// shrinkDefaultRoute returns an IPSet representing the IPs in route,
|
||||
@@ -1002,7 +1019,7 @@ func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
|
||||
for _, pfx := range removeFromDefaultRoute {
|
||||
b.RemovePrefix(pfx)
|
||||
}
|
||||
return b.IPSet(), nil
|
||||
return b.IPSet()
|
||||
}
|
||||
|
||||
// dnsCIDRsEqual determines whether two CIDR lists are equal
|
||||
@@ -1694,9 +1711,45 @@ func (b *LocalBackend) authReconfig() {
|
||||
|
||||
rcfg := b.routerConfig(cfg, uc)
|
||||
|
||||
var dcfg dns.Config
|
||||
dcfg := dns.Config{
|
||||
Routes: map[dnsname.FQDN][]netaddr.IPPort{},
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
}
|
||||
|
||||
// Populate MagicDNS records. We do this unconditionally so that
|
||||
// quad-100 can always respond to MagicDNS queries, even if the OS
|
||||
// isn't configured to make MagicDNS resolution truly
|
||||
// magic. Details in
|
||||
// https://github.com/tailscale/tailscale/issues/1886.
|
||||
set := func(name string, addrs []netaddr.IPPrefix) {
|
||||
if len(addrs) == 0 || name == "" {
|
||||
return
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(name)
|
||||
if err != nil {
|
||||
return // TODO: propagate error?
|
||||
}
|
||||
var ips []netaddr.IP
|
||||
for _, addr := range addrs {
|
||||
// Remove IPv6 addresses for now, as we don't
|
||||
// guarantee that the peer node actually can speak
|
||||
// IPv6 correctly.
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/1152
|
||||
// tracks adding the right capability reporting to
|
||||
// enable AAAA in MagicDNS.
|
||||
if addr.IP().Is6() {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, addr.IP())
|
||||
}
|
||||
dcfg.Hosts[fqdn] = ips
|
||||
}
|
||||
set(nm.Name, nm.Addresses)
|
||||
for _, peer := range nm.Peers {
|
||||
set(peer.Name, peer.Addresses)
|
||||
}
|
||||
|
||||
// If CorpDNS is false, dcfg remains the zero value.
|
||||
if uc.CorpDNS {
|
||||
addDefault := func(resolvers []tailcfg.DNSResolver) {
|
||||
for _, resolver := range resolvers {
|
||||
@@ -1710,9 +1763,6 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
|
||||
addDefault(nm.DNS.Resolvers)
|
||||
if len(nm.DNS.Routes) > 0 {
|
||||
dcfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{}
|
||||
}
|
||||
for suffix, resolvers := range nm.DNS.Routes {
|
||||
fqdn, err := dnsname.ToFQDN(suffix)
|
||||
if err != nil {
|
||||
@@ -1734,36 +1784,9 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
|
||||
}
|
||||
set := func(name string, addrs []netaddr.IPPrefix) {
|
||||
if len(addrs) == 0 || name == "" {
|
||||
return
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(name)
|
||||
if err != nil {
|
||||
return // TODO: propagate error?
|
||||
}
|
||||
var ips []netaddr.IP
|
||||
for _, addr := range addrs {
|
||||
// Remove IPv6 addresses for now, as we don't
|
||||
// guarantee that the peer node actually can speak
|
||||
// IPv6 correctly.
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/1152
|
||||
// tracks adding the right capability reporting to
|
||||
// enable AAAA in MagicDNS.
|
||||
if addr.IP().Is6() {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, addr.IP())
|
||||
}
|
||||
dcfg.Hosts[fqdn] = ips
|
||||
}
|
||||
if nm.DNS.Proxied { // actually means "enable MagicDNS"
|
||||
dcfg.AuthoritativeSuffixes = magicDNSRootDomains(nm)
|
||||
dcfg.Hosts = map[dnsname.FQDN][]netaddr.IP{}
|
||||
set(nm.Name, nm.Addresses)
|
||||
for _, peer := range nm.Peers {
|
||||
set(peer.Name, peer.Addresses)
|
||||
for _, dom := range magicDNSRootDomains(nm) {
|
||||
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1787,7 +1810,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/1713
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
case len(dcfg.Routes) == 0 && len(dcfg.Hosts) == 0 && len(dcfg.AuthoritativeSuffixes) == 0:
|
||||
case len(dcfg.Routes) == 0:
|
||||
// No settings requiring split DNS, no problem.
|
||||
case version.OS() == "android":
|
||||
// We don't support split DNS at all on Android yet.
|
||||
@@ -1815,8 +1838,9 @@ func parseResolver(cfg tailcfg.DNSResolver) (netaddr.IPPort, error) {
|
||||
// tailscaleVarRoot returns the root directory of Tailscale's writable
|
||||
// storage area. (e.g. "/var/lib/tailscale")
|
||||
func tailscaleVarRoot() string {
|
||||
if runtime.GOOS == "ios" {
|
||||
dir, _ := paths.IOSSharedDir.Load().(string)
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android":
|
||||
dir, _ := paths.AppSharedDir.Load().(string)
|
||||
return dir
|
||||
}
|
||||
stateFile := paths.DefaultTailscaledStateFile()
|
||||
@@ -1864,6 +1888,15 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.netMap == nil {
|
||||
// We're called from authReconfig which checks that
|
||||
// netMap is non-nil, but if a concurrent Logout,
|
||||
// ResetForClientDisconnect, or Start happens when its
|
||||
// mutex was released, the netMap could be
|
||||
// nil'ed out (Issue 1996). Bail out early here if so.
|
||||
return
|
||||
}
|
||||
|
||||
if len(b.netMap.Addresses) == len(b.peerAPIListeners) {
|
||||
allSame := true
|
||||
for i, pln := range b.peerAPIListeners {
|
||||
@@ -1920,7 +1953,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
// ("peerAPIListeners too low").
|
||||
continue
|
||||
}
|
||||
b.logf("[unexpected] peerapi listen(%q) error: %v", a.IP, err)
|
||||
b.logf("[unexpected] peerapi listen(%q) error: %v", a.IP(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -2019,6 +2052,11 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
|
||||
Routes: peerRoutes(cfg.Peers, 10_000),
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
// Issue 1995: we don't use iptables on Synology.
|
||||
rs.NetfilterMode = preftype.NetfilterOff
|
||||
}
|
||||
|
||||
// Sanity check: we expect the control server to program both a v4
|
||||
// and a v6 default route, if default routing is on. Fill in
|
||||
// blackhole routes appropriately if we're missing some. This is
|
||||
@@ -2312,7 +2350,6 @@ func (b *LocalBackend) LogoutSync(ctx context.Context) error {
|
||||
func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
b.setNetMapLocked(nil)
|
||||
b.mu.Unlock()
|
||||
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
@@ -2339,10 +2376,6 @@ func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
|
||||
cc.StartLogout()
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.setNetMapLocked(nil)
|
||||
b.mu.Unlock()
|
||||
|
||||
b.stateMachine()
|
||||
return err
|
||||
}
|
||||
@@ -2544,6 +2577,42 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// SetDNS adds a DNS record for the given domain name & TXT record
|
||||
// value.
|
||||
//
|
||||
// It's meant for use with dns-01 ACME (LetsEncrypt) challenges.
|
||||
//
|
||||
// This is the low-level interface. Other layers will provide more
|
||||
// friendly options to get HTTPS certs.
|
||||
func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
|
||||
req := &tailcfg.SetDNSRequest{
|
||||
Version: 1,
|
||||
Type: "TXT",
|
||||
Name: name,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
if prefs := b.prefs; prefs != nil {
|
||||
req.NodeKey = tailcfg.NodeKey(prefs.Persist.PrivateNodeKey.Public())
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if cc == nil {
|
||||
return errors.New("not connected")
|
||||
}
|
||||
if req.NodeKey.IsZero() {
|
||||
return errors.New("no nodekey")
|
||||
}
|
||||
if name == "" {
|
||||
return errors.New("missing 'name'")
|
||||
}
|
||||
if value == "" {
|
||||
return errors.New("missing 'value'")
|
||||
}
|
||||
return cc.SetDNS(ctx, req)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,redo ios,redo
|
||||
// +build darwin,ts_macext ios,ts_macext
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -140,6 +140,8 @@ func (cc *mockControl) send(err error, url string, loginFinished bool, nm *netma
|
||||
}
|
||||
if loginFinished {
|
||||
s.LoginFinished = &empty.Message{}
|
||||
} else if url == "" && err == nil && nm == nil {
|
||||
s.LogoutFinished = &empty.Message{}
|
||||
}
|
||||
cc.statusFunc(s)
|
||||
}
|
||||
@@ -246,6 +248,10 @@ func (cc *mockControl) UpdateEndpoints(localPort uint16, endpoints []tailcfg.End
|
||||
cc.called("UpdateEndpoints")
|
||||
}
|
||||
|
||||
func (*mockControl) SetDNS(context.Context, *tailcfg.SetDNSRequest) error {
|
||||
panic("unexpected SetDNS call")
|
||||
}
|
||||
|
||||
// A very precise test of the sequence of function calls generated by
|
||||
// ipnlocal.Local into its controlclient instance, and the events it
|
||||
// produces upstream into the UI.
|
||||
@@ -548,10 +554,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].NetMap, qt.Not(qt.IsNil))
|
||||
// BUG: Prefs should be sent too, or the UI could end up in
|
||||
// a bad state. (iOS, the only current user of this feature,
|
||||
// probably wouldn't notice because it happens to not display
|
||||
// any prefs. Maybe exit nodes will look weird?)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
}
|
||||
|
||||
// undo the state hack above.
|
||||
@@ -563,24 +566,25 @@ func TestStateMachine(t *testing.T) {
|
||||
b.Logout()
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
// BUG: now is not the time to unpause.
|
||||
c.Assert([]string{"unpause", "StartLogout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"pause", "StartLogout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
c.Assert(ipn.Stopped, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Let's make the logout succeed.
|
||||
t.Logf("\n\nLogout (async) - succeed")
|
||||
notifies.expect(0)
|
||||
notifies.expect(1)
|
||||
cc.setAuthBlocked(true)
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
nn := notifies.drain(1)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
|
||||
@@ -100,6 +100,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveBugReport(w, r)
|
||||
case "/localapi/v0/file-targets":
|
||||
h.serveFileTargets(w, r)
|
||||
case "/localapi/v0/set-dns":
|
||||
h.serveSetDNS(w, r)
|
||||
case "/":
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
default:
|
||||
@@ -382,6 +384,25 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
rp.ServeHTTP(w, outReq)
|
||||
}
|
||||
|
||||
func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", 400)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
err := h.b.SetDNS(ctx, r.FormValue("name"), r.FormValue("value"))
|
||||
if err != nil {
|
||||
writeErrorJSON(w, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(struct{}{})
|
||||
}
|
||||
|
||||
var dialPeerTransportOnce struct {
|
||||
sync.Once
|
||||
v *http.Transport
|
||||
|
||||
@@ -104,7 +104,9 @@ func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(Notify)) *
|
||||
b: b,
|
||||
sendNotifyMsg: sendNotifyMsg,
|
||||
}
|
||||
if sendNotifyMsg != nil {
|
||||
// b may be nil if the BackendServer is being created just to
|
||||
// encapsulate and send an error message.
|
||||
if sendNotifyMsg != nil && b != nil {
|
||||
b.SetNotifyCallback(bs.send)
|
||||
}
|
||||
return bs
|
||||
|
||||
@@ -187,3 +187,17 @@ func TestClientServer(t *testing.T) {
|
||||
})
|
||||
flushUntil(Running)
|
||||
}
|
||||
|
||||
func TestNilBackend(t *testing.T) {
|
||||
var called *Notify
|
||||
bs := NewBackendServer(t.Logf, nil, func(n Notify) {
|
||||
called = &n
|
||||
})
|
||||
bs.SendErrorMessage("Danger, Will Robinson!")
|
||||
if called == nil {
|
||||
t.Errorf("expect callback to be called, wasn't")
|
||||
}
|
||||
if called.ErrMessage == nil || *called.ErrMessage != "Danger, Will Robinson!" {
|
||||
t.Errorf("callback got wrong error: %v", called.ErrMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
@@ -72,7 +73,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
}
|
||||
l := &Logger{
|
||||
stderr: cfg.Stderr,
|
||||
stderrLevel: cfg.StderrLevel,
|
||||
stderrLevel: int64(cfg.StderrLevel),
|
||||
httpc: cfg.HTTPC,
|
||||
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
|
||||
lowMem: cfg.LowMemory,
|
||||
@@ -103,7 +104,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
// logging facilities and uploading to a log server.
|
||||
type Logger struct {
|
||||
stderr io.Writer
|
||||
stderrLevel int
|
||||
stderrLevel int64 // accessed atomically
|
||||
httpc *http.Client
|
||||
url string
|
||||
lowMem bool
|
||||
@@ -125,10 +126,8 @@ type Logger struct {
|
||||
// SetVerbosityLevel controls the verbosity level that should be
|
||||
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
|
||||
// are increasingly verbose.
|
||||
//
|
||||
// It should not be changed concurrently with log writes.
|
||||
func (l *Logger) SetVerbosityLevel(level int) {
|
||||
l.stderrLevel = level
|
||||
atomic.StoreInt64(&l.stderrLevel, int64(level))
|
||||
}
|
||||
|
||||
// SetLinkMonitor sets the optional the link monitor.
|
||||
@@ -514,7 +513,7 @@ func (l *Logger) Write(buf []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
level, buf := parseAndRemoveLogLevel(buf)
|
||||
if l.stderr != nil && l.stderr != ioutil.Discard && level <= l.stderrLevel {
|
||||
if l.stderr != nil && l.stderr != ioutil.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
|
||||
if buf[len(buf)-1] == '\n' {
|
||||
l.stderr.Write(buf)
|
||||
} else {
|
||||
|
||||
@@ -22,27 +22,26 @@ type Config struct {
|
||||
// for queries that fall within that suffix.
|
||||
// If a query doesn't match any entry in Routes, the
|
||||
// DefaultResolvers are used.
|
||||
// A Routes entry with no resolvers means the route should be
|
||||
// authoritatively answered using the contents of Hosts.
|
||||
Routes map[dnsname.FQDN][]netaddr.IPPort
|
||||
// SearchDomains are DNS suffixes to try when expanding
|
||||
// single-label queries.
|
||||
SearchDomains []dnsname.FQDN
|
||||
// Hosts maps DNS FQDNs to their IPs, which can be a mix of IPv4
|
||||
// and IPv6.
|
||||
// Queries matching entries in Hosts are resolved locally without
|
||||
// recursing off-machine.
|
||||
// Queries matching entries in Hosts are resolved locally by
|
||||
// 100.100.100.100 without leaving the machine.
|
||||
// Adding an entry to Hosts merely creates the record. If you want
|
||||
// it to resolve, you also need to add appropriate routes to
|
||||
// Routes.
|
||||
Hosts map[dnsname.FQDN][]netaddr.IP
|
||||
// AuthoritativeSuffixes is a list of fully-qualified DNS suffixes
|
||||
// for which the in-process Tailscale resolver is authoritative.
|
||||
// Queries for names within AuthoritativeSuffixes can only be
|
||||
// fulfilled by entries in Hosts. Queries with no match in Hosts
|
||||
// return NXDOMAIN.
|
||||
AuthoritativeSuffixes []dnsname.FQDN
|
||||
}
|
||||
|
||||
// needsAnyResolvers reports whether c requires a resolver to be set
|
||||
// at the OS level.
|
||||
func (c Config) needsOSResolver() bool {
|
||||
return c.hasDefaultResolvers() || c.hasRoutes() || c.hasHosts()
|
||||
return c.hasDefaultResolvers() || c.hasRoutes()
|
||||
}
|
||||
|
||||
func (c Config) hasRoutes() bool {
|
||||
@@ -52,7 +51,7 @@ func (c Config) hasRoutes() bool {
|
||||
// hasDefaultResolversOnly reports whether the only resolvers in c are
|
||||
// DefaultResolvers.
|
||||
func (c Config) hasDefaultResolversOnly() bool {
|
||||
return c.hasDefaultResolvers() && !c.hasRoutes() && !c.hasHosts()
|
||||
return c.hasDefaultResolvers() && !c.hasRoutes()
|
||||
}
|
||||
|
||||
func (c Config) hasDefaultResolvers() bool {
|
||||
@@ -63,44 +62,28 @@ func (c Config) hasDefaultResolvers() bool {
|
||||
// routes use the same resolvers, or nil if multiple sets of resolvers
|
||||
// are specified.
|
||||
func (c Config) singleResolverSet() []netaddr.IPPort {
|
||||
var first []netaddr.IPPort
|
||||
var (
|
||||
prev []netaddr.IPPort
|
||||
prevInitialized bool
|
||||
)
|
||||
for _, resolvers := range c.Routes {
|
||||
if first == nil {
|
||||
first = resolvers
|
||||
if !prevInitialized {
|
||||
prev = resolvers
|
||||
prevInitialized = true
|
||||
continue
|
||||
}
|
||||
if !sameIPPorts(first, resolvers) {
|
||||
if !sameIPPorts(prev, resolvers) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return first
|
||||
return prev
|
||||
}
|
||||
|
||||
// hasHosts reports whether c requires resolution of MagicDNS hosts or
|
||||
// domains.
|
||||
func (c Config) hasHosts() bool {
|
||||
return len(c.Hosts) > 0 || len(c.AuthoritativeSuffixes) > 0
|
||||
}
|
||||
|
||||
// matchDomains returns the list of match suffixes needed by Routes,
|
||||
// AuthoritativeSuffixes. Hosts is not considered as we assume that
|
||||
// they're covered by AuthoritativeSuffixes for now.
|
||||
// matchDomains returns the list of match suffixes needed by Routes.
|
||||
func (c Config) matchDomains() []dnsname.FQDN {
|
||||
ret := make([]dnsname.FQDN, 0, len(c.Routes)+len(c.AuthoritativeSuffixes))
|
||||
seen := map[dnsname.FQDN]bool{}
|
||||
for _, suffix := range c.AuthoritativeSuffixes {
|
||||
if seen[suffix] {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, suffix)
|
||||
seen[suffix] = true
|
||||
}
|
||||
ret := make([]dnsname.FQDN, 0, len(c.Routes))
|
||||
for suffix := range c.Routes {
|
||||
if seen[suffix] {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, suffix)
|
||||
seen[suffix] = true
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool {
|
||||
return ret[i].WithTrailingDot() < ret[j].WithTrailingDot()
|
||||
|
||||
@@ -6,7 +6,6 @@ package dns
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -75,40 +74,40 @@ func (m *Manager) Set(cfg Config) error {
|
||||
|
||||
// compileConfig converts cfg into a quad-100 resolver configuration
|
||||
// and an OS-level configuration.
|
||||
func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
|
||||
func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig, err error) {
|
||||
// The internal resolver always gets MagicDNS hosts and
|
||||
// authoritative suffixes, even if we don't propagate MagicDNS to
|
||||
// the OS.
|
||||
rcfg.Hosts = cfg.Hosts
|
||||
routes := map[dnsname.FQDN][]netaddr.IPPort{} // assigned conditionally to rcfg.Routes below.
|
||||
for suffix, resolvers := range cfg.Routes {
|
||||
if len(resolvers) == 0 {
|
||||
rcfg.LocalDomains = append(rcfg.LocalDomains, suffix)
|
||||
} else {
|
||||
routes[suffix] = resolvers
|
||||
}
|
||||
}
|
||||
// Similarly, the OS always gets search paths.
|
||||
ocfg.SearchDomains = cfg.SearchDomains
|
||||
|
||||
// Deal with trivial configs first.
|
||||
switch {
|
||||
case !cfg.needsOSResolver():
|
||||
// Set search domains, but nothing else. This also covers the
|
||||
// case where cfg is entirely zero, in which case these
|
||||
// configs clear all Tailscale DNS settings.
|
||||
return resolver.Config{}, OSConfig{
|
||||
SearchDomains: cfg.SearchDomains,
|
||||
}, nil
|
||||
return rcfg, ocfg, nil
|
||||
case cfg.hasDefaultResolversOnly():
|
||||
// Trivial CorpDNS configuration, just override the OS
|
||||
// resolver.
|
||||
return resolver.Config{}, OSConfig{
|
||||
Nameservers: toIPsOnly(cfg.DefaultResolvers),
|
||||
SearchDomains: cfg.SearchDomains,
|
||||
}, nil
|
||||
ocfg.Nameservers = toIPsOnly(cfg.DefaultResolvers)
|
||||
return rcfg, ocfg, nil
|
||||
case cfg.hasDefaultResolvers():
|
||||
// Default resolvers plus other stuff always ends up proxying
|
||||
// through quad-100.
|
||||
rcfg := resolver.Config{
|
||||
Routes: map[dnsname.FQDN][]netaddr.IPPort{
|
||||
".": cfg.DefaultResolvers,
|
||||
},
|
||||
Hosts: cfg.Hosts,
|
||||
LocalDomains: cfg.AuthoritativeSuffixes,
|
||||
}
|
||||
for suffix, resolvers := range cfg.Routes {
|
||||
rcfg.Routes[suffix] = resolvers
|
||||
}
|
||||
ocfg := OSConfig{
|
||||
Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()},
|
||||
SearchDomains: cfg.SearchDomains,
|
||||
}
|
||||
rcfg.Routes = routes
|
||||
rcfg.Routes["."] = cfg.DefaultResolvers
|
||||
ocfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
|
||||
return rcfg, ocfg, nil
|
||||
}
|
||||
|
||||
@@ -116,8 +115,6 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
|
||||
// configurations. The possible cases don't return directly any
|
||||
// more, because as a final step we have to handle the case where
|
||||
// the OS can't do split DNS.
|
||||
var rcfg resolver.Config
|
||||
var ocfg OSConfig
|
||||
|
||||
// Workaround for
|
||||
// https://github.com/tailscale/corp/issues/1662. Even though
|
||||
@@ -135,35 +132,19 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
|
||||
// This bool is used in a couple of places below to implement this
|
||||
// workaround.
|
||||
isWindows := runtime.GOOS == "windows"
|
||||
|
||||
// The windows check is for
|
||||
// . See also below
|
||||
// for further routing workarounds there.
|
||||
if !cfg.hasHosts() && cfg.singleResolverSet() != nil && m.os.SupportsSplitDNS() && !isWindows {
|
||||
if cfg.singleResolverSet() != nil && m.os.SupportsSplitDNS() && !isWindows {
|
||||
// Split DNS configuration requested, where all split domains
|
||||
// go to the same resolvers. We can let the OS do it.
|
||||
return resolver.Config{}, OSConfig{
|
||||
Nameservers: toIPsOnly(cfg.singleResolverSet()),
|
||||
SearchDomains: cfg.SearchDomains,
|
||||
MatchDomains: cfg.matchDomains(),
|
||||
}, nil
|
||||
ocfg.Nameservers = toIPsOnly(cfg.singleResolverSet())
|
||||
ocfg.MatchDomains = cfg.matchDomains()
|
||||
return rcfg, ocfg, nil
|
||||
}
|
||||
|
||||
// Split DNS configuration with either multiple upstream routes,
|
||||
// or routes + MagicDNS, or just MagicDNS, or on an OS that cannot
|
||||
// split-DNS. Install a split config pointing at quad-100.
|
||||
rcfg = resolver.Config{
|
||||
Hosts: cfg.Hosts,
|
||||
LocalDomains: cfg.AuthoritativeSuffixes,
|
||||
Routes: map[dnsname.FQDN][]netaddr.IPPort{},
|
||||
}
|
||||
for suffix, resolvers := range cfg.Routes {
|
||||
rcfg.Routes[suffix] = resolvers
|
||||
}
|
||||
ocfg = OSConfig{
|
||||
Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()},
|
||||
SearchDomains: cfg.SearchDomains,
|
||||
}
|
||||
rcfg.Routes = routes
|
||||
ocfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
|
||||
|
||||
// If the OS can't do native split-dns, read out the underlying
|
||||
// resolver config and blend it into our config.
|
||||
@@ -173,28 +154,7 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
|
||||
if !m.os.SupportsSplitDNS() || isWindows {
|
||||
bcfg, err := m.os.GetBaseConfig()
|
||||
if err != nil {
|
||||
// Temporary hack to make OSes where split-DNS isn't fully
|
||||
// implemented yet not completely crap out, but instead
|
||||
// fall back to quad-9 as a hardcoded "backup resolver".
|
||||
//
|
||||
// This codepath currently only triggers when opted into
|
||||
// the split-DNS feature server side, and when at least
|
||||
// one search domain is something within tailscale.com, so
|
||||
// we don't accidentally leak unstable user DNS queries to
|
||||
// quad-9 if we accidentally go down this codepath.
|
||||
canUseHack := false
|
||||
for _, dom := range cfg.SearchDomains {
|
||||
if strings.HasSuffix(dom.WithoutTrailingDot(), ".tailscale.com") {
|
||||
canUseHack = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !canUseHack {
|
||||
return resolver.Config{}, OSConfig{}, err
|
||||
}
|
||||
bcfg = OSConfig{
|
||||
Nameservers: []netaddr.IP{netaddr.IPv4(9, 9, 9, 9)},
|
||||
}
|
||||
return resolver.Config{}, OSConfig{}, err
|
||||
}
|
||||
rcfg.Routes["."] = toIPPorts(bcfg.Nameservers)
|
||||
ocfg.SearchDomains = append(ocfg.SearchDomains, bcfg.SearchDomains...)
|
||||
|
||||
@@ -76,6 +76,20 @@ func TestManager(t *testing.T) {
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Regression test for https://github.com/tailscale/tailscale/issues/1886
|
||||
name: "hosts-only",
|
||||
in: Config{
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "corp",
|
||||
in: Config{
|
||||
@@ -104,10 +118,10 @@ func TestManager(t *testing.T) {
|
||||
in: Config{
|
||||
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
Routes: upstreams("ts.com", ""),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
AuthoritativeSuffixes: fqdns("ts.com"),
|
||||
},
|
||||
os: OSConfig{
|
||||
Nameservers: mustIPs("100.100.100.100"),
|
||||
@@ -126,10 +140,10 @@ func TestManager(t *testing.T) {
|
||||
in: Config{
|
||||
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
Routes: upstreams("ts.com", ""),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
AuthoritativeSuffixes: fqdns("ts.com"),
|
||||
},
|
||||
split: true,
|
||||
os: OSConfig{
|
||||
@@ -261,8 +275,8 @@ func TestManager(t *testing.T) {
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
AuthoritativeSuffixes: fqdns("ts.com"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
Routes: upstreams("ts.com", ""),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
bs: OSConfig{
|
||||
Nameservers: mustIPs("8.8.8.8"),
|
||||
@@ -286,8 +300,8 @@ func TestManager(t *testing.T) {
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
AuthoritativeSuffixes: fqdns("ts.com"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
Routes: upstreams("ts.com", ""),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
split: true,
|
||||
os: OSConfig{
|
||||
@@ -305,12 +319,11 @@ func TestManager(t *testing.T) {
|
||||
{
|
||||
name: "routes-magic",
|
||||
in: Config{
|
||||
Routes: upstreams("corp.com", "2.2.2.2:53"),
|
||||
Routes: upstreams("corp.com", "2.2.2.2:53", "ts.com", ""),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
AuthoritativeSuffixes: fqdns("ts.com"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
bs: OSConfig{
|
||||
Nameservers: mustIPs("8.8.8.8"),
|
||||
@@ -333,12 +346,13 @@ func TestManager(t *testing.T) {
|
||||
{
|
||||
name: "routes-magic-split",
|
||||
in: Config{
|
||||
Routes: upstreams("corp.com", "2.2.2.2:53"),
|
||||
Routes: upstreams(
|
||||
"corp.com", "2.2.2.2:53",
|
||||
"ts.com", ""),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
AuthoritativeSuffixes: fqdns("ts.com"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
split: true,
|
||||
os: OSConfig{
|
||||
@@ -429,7 +443,12 @@ func upstreams(strs ...string) (ret map[dnsname.FQDN][]netaddr.IPPort) {
|
||||
var key dnsname.FQDN
|
||||
ret = map[dnsname.FQDN][]netaddr.IPPort{}
|
||||
for _, s := range strs {
|
||||
if ipp, err := netaddr.ParseIPPort(s); err == nil {
|
||||
if s == "" {
|
||||
if key == "" {
|
||||
panic("IPPort provided before suffix")
|
||||
}
|
||||
ret[key] = nil
|
||||
} else if ipp, err := netaddr.ParseIPPort(s); err == nil {
|
||||
if key == "" {
|
||||
panic("IPPort provided before suffix")
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
@@ -193,8 +194,14 @@ func (f *forwarder) recv(conn *fwdConn) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
out := make([]byte, maxResponseBytes)
|
||||
// The 1 extra byte is to detect packet truncation.
|
||||
out := make([]byte, maxResponseBytes+1)
|
||||
n := conn.read(out)
|
||||
var truncated bool
|
||||
if n > maxResponseBytes {
|
||||
n = maxResponseBytes
|
||||
truncated = true
|
||||
}
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -205,6 +212,19 @@ func (f *forwarder) recv(conn *fwdConn) {
|
||||
out = out[:n]
|
||||
txid := getTxID(out)
|
||||
|
||||
if truncated {
|
||||
const dnsFlagTruncated = 0x200
|
||||
flags := binary.BigEndian.Uint16(out[2:4])
|
||||
flags |= dnsFlagTruncated
|
||||
binary.BigEndian.PutUint16(out[2:4], flags)
|
||||
|
||||
// TODO(#2067): Remove any incomplete records? RFC 1035 section 6.2
|
||||
// states that truncation should head drop so that the authority
|
||||
// section can be preserved if possible. However, the UDP read with
|
||||
// a too-small buffer has already dropped the end, so that's the
|
||||
// best we can do.
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
|
||||
record, found := f.txMap[txid]
|
||||
@@ -286,6 +306,8 @@ func (f *forwarder) forward(query packet) error {
|
||||
}
|
||||
f.mu.Unlock()
|
||||
|
||||
// TODO(#2066): EDNS size clamping
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
f.send(query.bs, resolver)
|
||||
}
|
||||
@@ -371,6 +393,12 @@ func (c *fwdConn) send(packet []byte, dst netaddr.IPPort) {
|
||||
backOff(err)
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, syscall.EHOSTUNREACH) {
|
||||
// "No route to host." The network stack is fine, but
|
||||
// can't talk to this destination. Not much we can do
|
||||
// about that, don't spam logs.
|
||||
return
|
||||
}
|
||||
if networkIsDown(err) {
|
||||
// Fail.
|
||||
c.logf("send: network is down")
|
||||
@@ -422,7 +450,7 @@ func (c *fwdConn) read(out []byte) int {
|
||||
c.mu.Unlock()
|
||||
|
||||
n, _, err := conn.ReadFrom(out)
|
||||
if err == nil {
|
||||
if err == nil || packetWasTruncated(err) {
|
||||
// Success.
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -23,3 +23,8 @@ func networkIsDown(err error) bool {
|
||||
func networkIsUnreachable(err error) bool {
|
||||
return errors.Is(err, networkUnreachable)
|
||||
}
|
||||
|
||||
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. It always returns false on this
|
||||
// platform.
|
||||
func packetWasTruncated(err error) bool { return false }
|
||||
|
||||
@@ -8,3 +8,8 @@ package resolver
|
||||
|
||||
func networkIsDown(err error) bool { return false }
|
||||
func networkIsUnreachable(err error) bool { return false }
|
||||
|
||||
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. It always returns false on this
|
||||
// platform.
|
||||
func packetWasTruncated(err error) bool { return false }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
@@ -27,3 +28,16 @@ func networkIsUnreachable(err error) bool {
|
||||
// difference between down and unreachable? Add comments.
|
||||
return false
|
||||
}
|
||||
|
||||
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. On Windows, Go's UDP RecvFrom
|
||||
// calls WSARecvFrom which returns the WSAEMSGSIZE error code when the received
|
||||
// datagram is larger than the provided buffer. When that happens, both a valid
|
||||
// size and an error are returned (as per the partial fix for golang/go#14074).
|
||||
// If the WSAEMSGSIZE error is returned, then we ignore the error to get
|
||||
// semantics similar to the POSIX operating systems. One caveat is that it
|
||||
// appears that the source address is not returned when WSAEMSGSIZE occurs, but
|
||||
// we do not currently look at the source address.
|
||||
func packetWasTruncated(err error) bool {
|
||||
return errors.Is(err, windows.WSAEMSGSIZE)
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ import (
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// maxResponseBytes is the maximum size of a response from a Resolver.
|
||||
const maxResponseBytes = 512
|
||||
// maxResponseBytes is the maximum size of a response from a Resolver. The
|
||||
// actual buffer size will be one larger than this so that we can detect
|
||||
// truncation in a platform-agnostic way.
|
||||
const maxResponseBytes = 4095
|
||||
|
||||
// queueSize is the maximal number of DNS requests that can await polling.
|
||||
// If EnqueueRequest is called when this many requests are already pending,
|
||||
|
||||
@@ -66,6 +66,39 @@ func resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveToTXT returns a handler function which responds to queries of type TXT
|
||||
// it receives with the strings in txts.
|
||||
func resolveToTXT(txts []string) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
|
||||
if len(req.Question) != 1 {
|
||||
panic("not a single-question request")
|
||||
}
|
||||
question := req.Question[0]
|
||||
|
||||
if question.Qtype != dns.TypeTXT {
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
ans := &dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Txt: txts,
|
||||
}
|
||||
|
||||
m.Answer = append(m.Answer, ans)
|
||||
if err := w.WriteMsg(m); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resolveToNXDOMAIN = dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(req, dns.RcodeNameError)
|
||||
|
||||
@@ -6,7 +6,9 @@ package resolver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
@@ -44,9 +46,11 @@ func dnspacket(domain dnsname.FQDN, tp dns.Type) []byte {
|
||||
}
|
||||
|
||||
type dnsResponse struct {
|
||||
ip netaddr.IP
|
||||
name dnsname.FQDN
|
||||
rcode dns.RCode
|
||||
ip netaddr.IP
|
||||
txt []string
|
||||
name dnsname.FQDN
|
||||
rcode dns.RCode
|
||||
truncated bool
|
||||
}
|
||||
|
||||
func unpackResponse(payload []byte) (dnsResponse, error) {
|
||||
@@ -67,6 +71,16 @@ func unpackResponse(payload []byte) (dnsResponse, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
response.truncated = h.Truncated
|
||||
if response.truncated {
|
||||
// TODO(#2067): Ideally, answer processing should still succeed when
|
||||
// dealing with a truncated message, but currently when we truncate
|
||||
// a packet, it's caused by the buffer being too small and usually that
|
||||
// means the data runs out mid-record. dns.Parser does not like it when
|
||||
// that happens. We can improve this by trimming off incomplete records.
|
||||
return response, nil
|
||||
}
|
||||
|
||||
err = parser.SkipAllQuestions()
|
||||
if err != nil {
|
||||
return response, err
|
||||
@@ -90,6 +104,12 @@ func unpackResponse(payload []byte) (dnsResponse, error) {
|
||||
return response, err
|
||||
}
|
||||
response.ip = netaddr.IPv6Raw(res.AAAA)
|
||||
case dns.TypeTXT:
|
||||
res, err := parser.TXTResource()
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
response.txt = res.TXT
|
||||
case dns.TypeNS:
|
||||
res, err := parser.NSResource()
|
||||
if err != nil {
|
||||
@@ -269,6 +289,32 @@ func ipv6Works() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func generateTXT(size int, source rand.Source) []string {
|
||||
const sizePerTXT = 120
|
||||
|
||||
if size%2 != 0 {
|
||||
panic("even lengths only")
|
||||
}
|
||||
|
||||
rng := rand.New(source)
|
||||
|
||||
txts := make([]string, 0, size/sizePerTXT+1)
|
||||
|
||||
raw := make([]byte, sizePerTXT/2)
|
||||
|
||||
rem := size
|
||||
for ; rem > sizePerTXT; rem -= sizePerTXT {
|
||||
rng.Read(raw)
|
||||
txts = append(txts, hex.EncodeToString(raw))
|
||||
}
|
||||
if rem > 0 {
|
||||
rng.Read(raw[:rem/2])
|
||||
txts = append(txts, hex.EncodeToString(raw[:rem/2]))
|
||||
}
|
||||
|
||||
return txts
|
||||
}
|
||||
|
||||
func TestDelegate(t *testing.T) {
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
@@ -276,13 +322,43 @@ func TestDelegate(t *testing.T) {
|
||||
t.Skip("skipping test that requires localhost IPv6")
|
||||
}
|
||||
|
||||
randSource := rand.NewSource(4)
|
||||
|
||||
// smallTXT does not require EDNS
|
||||
smallTXT := generateTXT(300, randSource)
|
||||
|
||||
// medTXT and largeTXT are responses that require EDNS but we would like to
|
||||
// support these sizes of response without truncation because they are
|
||||
// moderately common.
|
||||
medTXT := generateTXT(1200, randSource)
|
||||
largeTXT := generateTXT(4000, randSource)
|
||||
|
||||
// xlargeTXT is slightly above the maximum response size that we support,
|
||||
// so there should be truncation.
|
||||
xlargeTXT := generateTXT(5000, randSource)
|
||||
|
||||
// hugeTXT is significantly larger than any typical MTU and will require
|
||||
// significant fragmentation. For buffer management reasons, we do not
|
||||
// intend to handle responses this large, so there should be truncation.
|
||||
hugeTXT := generateTXT(64000, randSource)
|
||||
|
||||
v4server := serveDNS(t, "127.0.0.1:0",
|
||||
"test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."),
|
||||
"nxdomain.site.", resolveToNXDOMAIN)
|
||||
"nxdomain.site.", resolveToNXDOMAIN,
|
||||
"small.txt.", resolveToTXT(smallTXT),
|
||||
"med.txt.", resolveToTXT(medTXT),
|
||||
"large.txt.", resolveToTXT(largeTXT),
|
||||
"xlarge.txt.", resolveToTXT(xlargeTXT),
|
||||
"huge.txt.", resolveToTXT(hugeTXT))
|
||||
defer v4server.Shutdown()
|
||||
v6server := serveDNS(t, "[::1]:0",
|
||||
"test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."),
|
||||
"nxdomain.site.", resolveToNXDOMAIN)
|
||||
"nxdomain.site.", resolveToNXDOMAIN,
|
||||
"small.txt.", resolveToTXT(smallTXT),
|
||||
"med.txt.", resolveToTXT(medTXT),
|
||||
"large.txt.", resolveToTXT(largeTXT),
|
||||
"xlarge.txt.", resolveToTXT(xlargeTXT),
|
||||
"huge.txt.", resolveToTXT(hugeTXT))
|
||||
defer v6server.Shutdown()
|
||||
|
||||
r := New(t.Logf, nil)
|
||||
@@ -322,6 +398,31 @@ func TestDelegate(t *testing.T) {
|
||||
dnspacket("nxdomain.site.", dns.TypeA),
|
||||
dnsResponse{rcode: dns.RCodeNameError},
|
||||
},
|
||||
{
|
||||
"smalltxt",
|
||||
dnspacket("small.txt.", dns.TypeTXT),
|
||||
dnsResponse{txt: smallTXT, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"medtxt",
|
||||
dnspacket("med.txt.", dns.TypeTXT),
|
||||
dnsResponse{txt: medTXT, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"largetxt",
|
||||
dnspacket("large.txt.", dns.TypeTXT),
|
||||
dnsResponse{txt: largeTXT, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"xlargetxt",
|
||||
dnspacket("xlarge.txt.", dns.TypeTXT),
|
||||
dnsResponse{rcode: dns.RCodeSuccess, truncated: true},
|
||||
},
|
||||
{
|
||||
"hugetxt",
|
||||
dnspacket("huge.txt.", dns.TypeTXT),
|
||||
dnsResponse{rcode: dns.RCodeSuccess, truncated: true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -345,6 +446,15 @@ func TestDelegate(t *testing.T) {
|
||||
if response.name != tt.response.name {
|
||||
t.Errorf("name = %v; want %v", response.name, tt.response.name)
|
||||
}
|
||||
if len(response.txt) != len(tt.response.txt) {
|
||||
t.Errorf("%v txt records, want %v txt records", len(response.txt), len(tt.response.txt))
|
||||
} else {
|
||||
for i := range response.txt {
|
||||
if response.txt[i] != tt.response.txt[i] {
|
||||
t.Errorf("txt record %v is %s, want %s", i, response.txt[i], tt.response.txt[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux,!redo
|
||||
// +build linux darwin,!ts_macext
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -23,64 +19,3 @@ func TestDefaultRouteInterface(t *testing.T) {
|
||||
}
|
||||
t.Logf("got %q", v)
|
||||
}
|
||||
|
||||
// test the specific /proc/net/route path as found on Google Cloud Run instances
|
||||
func TestGoogleCloudRunDefaultRouteInterface(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
savedProcNetRoutePath := procNetRoutePath
|
||||
defer func() { procNetRoutePath = savedProcNetRoutePath }()
|
||||
procNetRoutePath = filepath.Join(dir, "CloudRun")
|
||||
buf := []byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
|
||||
"eth0\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n" +
|
||||
"eth1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n")
|
||||
err := ioutil.WriteFile(procNetRoutePath, buf, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "eth1" {
|
||||
t.Fatalf("got %s, want eth1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// we read chunks of /proc/net/route at a time, test that files longer than the chunk
|
||||
// size can be handled.
|
||||
func TestExtremelyLongProcNetRoute(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
savedProcNetRoutePath := procNetRoutePath
|
||||
defer func() { procNetRoutePath = savedProcNetRoutePath }()
|
||||
procNetRoutePath = filepath.Join(dir, "VeryLong")
|
||||
f, err := os.Create(procNetRoutePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = f.Write([]byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for n := 0; n <= 1000; n++ {
|
||||
line := fmt.Sprintf("eth%d\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n", n)
|
||||
_, err := f.Write([]byte(line))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
_, err = f.Write([]byte("tokenring1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "tokenring1" {
|
||||
t.Fatalf("got %q, want tokenring1", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,74 @@
|
||||
|
||||
package interfaces
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// test the specific /proc/net/route path as found on Google Cloud Run instances
|
||||
func TestGoogleCloudRunDefaultRouteInterface(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
savedProcNetRoutePath := procNetRoutePath
|
||||
defer func() { procNetRoutePath = savedProcNetRoutePath }()
|
||||
procNetRoutePath = filepath.Join(dir, "CloudRun")
|
||||
buf := []byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
|
||||
"eth0\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n" +
|
||||
"eth1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n")
|
||||
err := ioutil.WriteFile(procNetRoutePath, buf, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "eth1" {
|
||||
t.Fatalf("got %s, want eth1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// we read chunks of /proc/net/route at a time, test that files longer than the chunk
|
||||
// size can be handled.
|
||||
func TestExtremelyLongProcNetRoute(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
savedProcNetRoutePath := procNetRoutePath
|
||||
defer func() { procNetRoutePath = savedProcNetRoutePath }()
|
||||
procNetRoutePath = filepath.Join(dir, "VeryLong")
|
||||
f, err := os.Create(procNetRoutePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = f.Write([]byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for n := 0; n <= 1000; n++ {
|
||||
line := fmt.Sprintf("eth%d\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n", n)
|
||||
_, err := f.Write([]byte(line))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
_, err = f.Write([]byte("tokenring1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "tokenring1" {
|
||||
t.Fatalf("got %q, want tokenring1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDefaultRouteInterface(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!redo
|
||||
// +build darwin,!ts_macext
|
||||
|
||||
package netns
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!windows,!darwin darwin,redo
|
||||
// +build !linux,!windows,!darwin darwin,ts_macext
|
||||
|
||||
package netns
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/strbuilder"
|
||||
)
|
||||
|
||||
const unknown = ipproto.Unknown
|
||||
@@ -62,36 +61,17 @@ func (p *Parsed) String() string {
|
||||
return "Unknown{???}"
|
||||
}
|
||||
|
||||
sb := strbuilder.Get()
|
||||
sb.WriteString(p.IPProto.String())
|
||||
sb.WriteByte('{')
|
||||
writeIPPort(sb, p.Src)
|
||||
sb.WriteString(" > ")
|
||||
writeIPPort(sb, p.Dst)
|
||||
sb.WriteByte('}')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// writeIPPort writes ipp.String() into sb, with fewer allocations.
|
||||
//
|
||||
// TODO: make netaddr more efficient in this area, and retire this func.
|
||||
func writeIPPort(sb *strbuilder.Builder, ipp netaddr.IPPort) {
|
||||
if ipp.IP().Is4() {
|
||||
raw := ipp.IP().As4()
|
||||
sb.WriteUint(uint64(raw[0]))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(raw[1]))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(raw[2]))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(raw[3]))
|
||||
sb.WriteByte(':')
|
||||
} else {
|
||||
sb.WriteByte('[')
|
||||
sb.WriteString(ipp.IP().String()) // TODO: faster?
|
||||
sb.WriteString("]:")
|
||||
}
|
||||
sb.WriteUint(uint64(ipp.Port()))
|
||||
// max is the maximum reasonable length of the string we are constructing.
|
||||
// It's OK to overshoot, as the temp buffer is allocated on the stack.
|
||||
const max = len("ICMPv6{[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0]:65535 > [ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0]:65535}")
|
||||
b := make([]byte, 0, max)
|
||||
b = append(b, p.IPProto.String()...)
|
||||
b = append(b, '{')
|
||||
b = p.Src.AppendTo(b)
|
||||
b = append(b, ' ', '>', ' ')
|
||||
b = p.Dst.AppendTo(b)
|
||||
b = append(b, '}')
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Decode extracts data from the packet in b into q.
|
||||
|
||||
@@ -378,11 +378,9 @@ func TestParsedString(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
var sink string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
sink = tests[0].qdecode.String()
|
||||
sinkString = tests[0].qdecode.String()
|
||||
})
|
||||
_ = sink
|
||||
if allocs != 1 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
@@ -532,3 +530,33 @@ func TestMarshalResponse(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var sinkString string
|
||||
|
||||
func BenchmarkString(b *testing.B) {
|
||||
benches := []struct {
|
||||
name string
|
||||
buf []byte
|
||||
}{
|
||||
{"tcp4", tcp4PacketBuffer},
|
||||
{"tcp6", tcp6RequestBuffer},
|
||||
{"udp4", udp4RequestBuffer},
|
||||
{"udp6", udp6RequestBuffer},
|
||||
{"icmp4", icmp4RequestBuffer},
|
||||
{"icmp6", icmp6PacketBuffer},
|
||||
{"igmp", igmpPacketBuffer},
|
||||
{"unknown", unknownPacketBuffer},
|
||||
}
|
||||
|
||||
for _, bench := range benches {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var p Parsed
|
||||
p.Decode(bench.buf)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkString = p.String()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,9 @@ var (
|
||||
// TailscaleServiceIP returns the listen address of services
|
||||
// provided by Tailscale itself such as the MagicDNS proxy.
|
||||
func TailscaleServiceIP() netaddr.IP {
|
||||
serviceIP.Do(func() { mustIP(&serviceIP.v, "100.100.100.100") })
|
||||
return serviceIP.v
|
||||
return netaddr.IPv4(100, 100, 100, 100) // "100.100.100.100" for those grepping
|
||||
}
|
||||
|
||||
var serviceIP onceIP
|
||||
|
||||
// IsTailscaleIP reports whether ip is an IP address in a range that
|
||||
// Tailscale assigns from.
|
||||
func IsTailscaleIP(ip netaddr.IP) bool {
|
||||
@@ -126,19 +123,6 @@ type oncePrefix struct {
|
||||
v netaddr.IPPrefix
|
||||
}
|
||||
|
||||
func mustIP(v *netaddr.IP, ip string) {
|
||||
var err error
|
||||
*v, err = netaddr.ParseIP(ip)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type onceIP struct {
|
||||
sync.Once
|
||||
v netaddr.IP
|
||||
}
|
||||
|
||||
// NewContainsIPFunc returns a func that reports whether ip is in addrs.
|
||||
//
|
||||
// It's optimized for the cases of addrs being empty and addrs
|
||||
|
||||
@@ -93,3 +93,11 @@ func TestNewContainsIPFunc(t *testing.T) {
|
||||
t.Fatal("bad")
|
||||
}
|
||||
}
|
||||
|
||||
var sinkIP netaddr.IP
|
||||
|
||||
func BenchmarkTailscaleServiceAddr(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkIP = TailscaleServiceIP()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
)
|
||||
|
||||
type fakeTUN struct {
|
||||
|
||||
@@ -9,7 +9,7 @@ package tstun
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -11,27 +11,34 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// minimalMTU is the MTU we set on tailscale's TUN
|
||||
// interface. wireguard-go defaults to 1420 bytes, which only works if
|
||||
// the "outer" MTU is 1500 bytes. This breaks on DSL connections
|
||||
// (typically 1492 MTU) and on GCE (1460 MTU?!).
|
||||
// tunMTU is the MTU we set on tailscale's TUN interface. wireguard-go
|
||||
// defaults to 1420 bytes, which only works if the "outer" MTU is 1500
|
||||
// bytes. This breaks on DSL connections (typically 1492 MTU) and on
|
||||
// GCE (1460 MTU?!).
|
||||
//
|
||||
// 1280 is the smallest MTU allowed for IPv6, which is a sensible
|
||||
// "probably works everywhere" setting until we develop proper PMTU
|
||||
// discovery.
|
||||
const minimalMTU = 1280
|
||||
var tunMTU = 1280
|
||||
|
||||
func init() {
|
||||
if mtu, _ := strconv.Atoi(os.Getenv("TS_DEBUG_MTU")); mtu != 0 {
|
||||
tunMTU = mtu
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a tun.Device for the requested device name, along with
|
||||
// the OS-dependent name that was allocated to the device.
|
||||
func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
|
||||
dev, err := tun.CreateTUN(tunName, minimalMTU)
|
||||
dev, err := tun.CreateTUN(tunName, tunMTU)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
package tstun
|
||||
|
||||
import "github.com/tailscale/wireguard-go/tun"
|
||||
import "golang.zx2c4.com/wireguard/tun"
|
||||
|
||||
func interfaceName(dev tun.Device) (string, error) {
|
||||
return dev.Name()
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
package tstun
|
||||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"github.com/tailscale/wireguard-go/tun/wintun"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"golang.zx2c4.com/wireguard/tun/wintun"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
)
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/types/ipproto"
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun/tuntest"
|
||||
"golang.zx2c4.com/wireguard/tun/tuntest"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/types/ipproto"
|
||||
@@ -146,7 +146,8 @@ func setfilter(logf logger.Logf, tun *Wrapper) {
|
||||
}
|
||||
var sb netaddr.IPSetBuilder
|
||||
sb.AddPrefix(netaddr.MustParseIPPrefix("1.2.0.0/16"))
|
||||
tun.SetFilter(filter.New(matches, sb.IPSet(), sb.IPSet(), nil, logf))
|
||||
ipSet, _ := sb.IPSet()
|
||||
tun.SetFilter(filter.New(matches, ipSet, ipSet, nil, logf))
|
||||
}
|
||||
|
||||
func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper) {
|
||||
|
||||
184
packages/deb/deb.go
Normal file
184
packages/deb/deb.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// 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 deb extracts metadata from Debian packages.
|
||||
package deb
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Info is the Debian package metadata needed to integrate the package
|
||||
// into a repository.
|
||||
type Info struct {
|
||||
// Version is the version of the package, as reported by dpkg.
|
||||
Version string
|
||||
// Arch is the Debian CPU architecture the package is for.
|
||||
Arch string
|
||||
// Control is the entire contents of the package's control file,
|
||||
// with leading and trailing whitespace removed.
|
||||
Control []byte
|
||||
// MD5 is the MD5 hash of the package file.
|
||||
MD5 []byte
|
||||
// SHA1 is the SHA1 hash of the package file.
|
||||
SHA1 []byte
|
||||
// SHA256 is the SHA256 hash of the package file.
|
||||
SHA256 []byte
|
||||
}
|
||||
|
||||
// ReadFile returns Debian package metadata from the .deb file at path.
|
||||
func ReadFile(path string) (*Info, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Read(f)
|
||||
}
|
||||
|
||||
// Read returns Debian package metadata from the .deb file in r.
|
||||
func Read(r io.Reader) (*Info, error) {
|
||||
b := bufio.NewReader(r)
|
||||
|
||||
m5, s1, s256 := md5.New(), sha1.New(), sha256.New()
|
||||
summers := io.MultiWriter(m5, s1, s256)
|
||||
r = io.TeeReader(b, summers)
|
||||
|
||||
t, err := findControlTar(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("searching for control.tar.gz: %w", err)
|
||||
}
|
||||
|
||||
control, err := findControlFile(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("searching for control file in control.tar.gz: %w", err)
|
||||
}
|
||||
|
||||
arch, version, err := findArchAndVersion(control)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extracting version and architecture from control file: %w", err)
|
||||
}
|
||||
|
||||
// Exhaust the remainder of r, so that the summers see the entire file.
|
||||
if _, err := io.Copy(ioutil.Discard, r); err != nil {
|
||||
return nil, fmt.Errorf("hashing file: %w", err)
|
||||
}
|
||||
|
||||
return &Info{
|
||||
Version: version,
|
||||
Arch: arch,
|
||||
Control: control,
|
||||
MD5: m5.Sum(nil),
|
||||
SHA1: s1.Sum(nil),
|
||||
SHA256: s256.Sum(nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// findControlTar reads r as an `ar` archive, finds a tarball named
|
||||
// `control.tar.gz` within, and returns a reader for that file.
|
||||
func findControlTar(r io.Reader) (tarReader io.Reader, err error) {
|
||||
var magic [8]byte
|
||||
if _, err := io.ReadFull(r, magic[:]); err != nil {
|
||||
return nil, fmt.Errorf("reading ar magic: %w", err)
|
||||
}
|
||||
if string(magic[:]) != "!<arch>\n" {
|
||||
return nil, fmt.Errorf("not an ar file (bad magic %q)", magic)
|
||||
}
|
||||
|
||||
for {
|
||||
var hdr [60]byte
|
||||
if _, err := io.ReadFull(r, hdr[:]); err != nil {
|
||||
return nil, fmt.Errorf("reading file header: %w", err)
|
||||
}
|
||||
filename := strings.TrimSpace(string(hdr[:16]))
|
||||
size, err := strconv.ParseInt(strings.TrimSpace(string(hdr[48:58])), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading size of file %q: %w", filename, err)
|
||||
}
|
||||
if filename == "control.tar.gz" {
|
||||
return io.LimitReader(r, size), nil
|
||||
}
|
||||
|
||||
// files in ar are padded out to 2 bytes.
|
||||
if size%2 == 1 {
|
||||
size++
|
||||
}
|
||||
if _, err := io.CopyN(ioutil.Discard, r, size); err != nil {
|
||||
return nil, fmt.Errorf("seeking past file %q: %w", filename, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findControlFile reads r as a tar.gz archive, finds a file named
|
||||
// `control` within, and returns its contents.
|
||||
func findControlFile(r io.Reader) (control []byte, err error) {
|
||||
gz, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decompressing control.tar.gz: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil, errors.New("EOF while looking for control file in control.tar.gz")
|
||||
}
|
||||
return nil, fmt.Errorf("reading tar header: %w", err)
|
||||
}
|
||||
|
||||
if filepath.Clean(hdr.Name) != "control" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found control file
|
||||
break
|
||||
}
|
||||
|
||||
bs, err := ioutil.ReadAll(tr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading control file: %w", err)
|
||||
}
|
||||
|
||||
return bytes.TrimSpace(bs), nil
|
||||
}
|
||||
|
||||
var (
|
||||
archKey = []byte("Architecture:")
|
||||
versionKey = []byte("Version:")
|
||||
)
|
||||
|
||||
// findArchAndVersion extracts the architecture and version strings
|
||||
// from the given control file.
|
||||
func findArchAndVersion(control []byte) (arch string, version string, err error) {
|
||||
b := bytes.NewBuffer(control)
|
||||
for {
|
||||
l, err := b.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if bytes.HasPrefix(l, archKey) {
|
||||
arch = string(bytes.TrimSpace(l[len(archKey):]))
|
||||
} else if bytes.HasPrefix(l, versionKey) {
|
||||
version = string(bytes.TrimSpace(l[len(versionKey):]))
|
||||
}
|
||||
if arch != "" && version != "" {
|
||||
return arch, version, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
202
packages/deb/deb_test.go
Normal file
202
packages/deb/deb_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// 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 deb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/goreleaser/nfpm"
|
||||
_ "github.com/goreleaser/nfpm/deb"
|
||||
)
|
||||
|
||||
func TestDebInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []byte
|
||||
want *Info
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
in: mkTestDeb("1.2.3", "amd64"),
|
||||
want: &Info{
|
||||
Version: "1.2.3",
|
||||
Arch: "amd64",
|
||||
Control: mkControl(
|
||||
"Package", "tailscale",
|
||||
"Version", "1.2.3",
|
||||
"Section", "net",
|
||||
"Priority", "extra",
|
||||
"Architecture", "amd64",
|
||||
"Installed-Size", "0",
|
||||
"Description", "test package"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arm64",
|
||||
in: mkTestDeb("1.2.3", "arm64"),
|
||||
want: &Info{
|
||||
Version: "1.2.3",
|
||||
Arch: "arm64",
|
||||
Control: mkControl(
|
||||
"Package", "tailscale",
|
||||
"Version", "1.2.3",
|
||||
"Section", "net",
|
||||
"Priority", "extra",
|
||||
"Architecture", "arm64",
|
||||
"Installed-Size", "0",
|
||||
"Description", "test package"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unstable",
|
||||
in: mkTestDeb("1.7.25", "amd64"),
|
||||
want: &Info{
|
||||
Version: "1.7.25",
|
||||
Arch: "amd64",
|
||||
Control: mkControl(
|
||||
"Package", "tailscale",
|
||||
"Version", "1.7.25",
|
||||
"Section", "net",
|
||||
"Priority", "extra",
|
||||
"Architecture", "amd64",
|
||||
"Installed-Size", "0",
|
||||
"Description", "test package"),
|
||||
},
|
||||
},
|
||||
|
||||
// These truncation tests assume the structure of a .deb
|
||||
// package, which is as follows:
|
||||
// magic: 8 bytes
|
||||
// file header: 60 bytes, before each file blob
|
||||
//
|
||||
// The first file in a .deb ar is "debian-binary", which is 4
|
||||
// bytes long and consists of "2.0\n".
|
||||
// The second file is control.tar.gz, which is what we care
|
||||
// about introspecting for metadata.
|
||||
// The final file is data.tar.gz, which we don't care about.
|
||||
//
|
||||
// The first file in control.tar.gz is the "control" file we
|
||||
// want to read for metadata.
|
||||
{
|
||||
name: "truncated_ar_magic",
|
||||
in: mkTestDeb("1.7.25", "amd64")[:4],
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "truncated_ar_header",
|
||||
in: mkTestDeb("1.7.25", "amd64")[:30],
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing_control_tgz",
|
||||
// Truncate right after the "debian-binary" file, which
|
||||
// makes the file a valid 1-file archive that's missing
|
||||
// control.tar.gz.
|
||||
in: mkTestDeb("1.7.25", "amd64")[:72],
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "truncated_tgz",
|
||||
in: mkTestDeb("1.7.25", "amd64")[:172],
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
// mkTestDeb returns non-deterministic output due to
|
||||
// timestamps embedded in the package file, so compute the
|
||||
// wanted hashes on the fly here.
|
||||
if test.want != nil {
|
||||
test.want.MD5 = mkHash(test.in, md5.New)
|
||||
test.want.SHA1 = mkHash(test.in, sha1.New)
|
||||
test.want.SHA256 = mkHash(test.in, sha256.New)
|
||||
}
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
b := bytes.NewBuffer(test.in)
|
||||
got, err := Read(b)
|
||||
if err != nil {
|
||||
if test.wantErr {
|
||||
t.Logf("got expected error: %v", err)
|
||||
return
|
||||
}
|
||||
t.Fatalf("reading deb info: %v", err)
|
||||
}
|
||||
if diff := diff(got, test.want); diff != "" {
|
||||
t.Fatalf("parsed info diff (-got+want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func diff(got, want interface{}) string {
|
||||
matchField := func(name string) func(p cmp.Path) bool {
|
||||
return func(p cmp.Path) bool {
|
||||
if len(p) != 3 {
|
||||
return false
|
||||
}
|
||||
return p[2].String() == "."+name
|
||||
}
|
||||
}
|
||||
toLines := cmp.Transformer("lines", func(b []byte) []string { return strings.Split(string(b), "\n") })
|
||||
toHex := cmp.Transformer("hex", func(b []byte) string { return hex.EncodeToString(b) })
|
||||
return cmp.Diff(got, want,
|
||||
cmp.FilterPath(matchField("Control"), toLines),
|
||||
cmp.FilterPath(matchField("MD5"), toHex),
|
||||
cmp.FilterPath(matchField("SHA1"), toHex),
|
||||
cmp.FilterPath(matchField("SHA256"), toHex))
|
||||
}
|
||||
|
||||
func mkTestDeb(version, arch string) []byte {
|
||||
info := nfpm.WithDefaults(&nfpm.Info{
|
||||
Name: "tailscale",
|
||||
Description: "test package",
|
||||
Arch: arch,
|
||||
Platform: "linux",
|
||||
Version: version,
|
||||
Section: "net",
|
||||
Priority: "extra",
|
||||
})
|
||||
|
||||
pkg, err := nfpm.Get("deb")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("getting deb packager: %v", err))
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := pkg.Package(info, &b); err != nil {
|
||||
panic(fmt.Sprintf("creating deb package: %v", err))
|
||||
}
|
||||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func mkControl(fs ...string) []byte {
|
||||
if len(fs)%2 != 0 {
|
||||
panic("odd number of control file fields")
|
||||
}
|
||||
var b bytes.Buffer
|
||||
for i := 0; i < len(fs); i = i + 2 {
|
||||
k, v := fs[i], fs[i+1]
|
||||
fmt.Fprintf(&b, "%s: %s\n", k, v)
|
||||
}
|
||||
return bytes.TrimSpace(b.Bytes())
|
||||
}
|
||||
|
||||
func mkHash(b []byte, hasher func() hash.Hash) []byte {
|
||||
h := hasher()
|
||||
h.Write(b)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
@@ -11,11 +11,13 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// IOSSharedDir is a string set by the iOS app on start
|
||||
// AppSharedDir is a string set by the iOS or Android app on start
|
||||
// containing a directory we can read/write in.
|
||||
var IOSSharedDir atomic.Value
|
||||
var AppSharedDir atomic.Value
|
||||
|
||||
// DefaultTailscaledSocket returns the path to the tailscaled Unix socket
|
||||
// or the empty string if there's no reasonable default.
|
||||
@@ -26,11 +28,15 @@ func DefaultTailscaledSocket() string {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return "/var/run/tailscaled.socket"
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
// TODO(crawshaw): does this path change with DSM7?
|
||||
const synologySock = "/volume1/@appstore/Tailscale/var/tailscaled.sock" // SYNOPKG_PKGDEST in scripts/installer
|
||||
if fi, err := os.Stat(filepath.Dir(synologySock)); err == nil && fi.IsDir() {
|
||||
return synologySock
|
||||
if distro.Get() == distro.Synology {
|
||||
// TODO(maisem): be smarter about this. We can parse /etc/VERSION.
|
||||
const dsm6Sock = "/var/packages/Tailscale/etc/tailscaled.sock"
|
||||
const dsm7Sock = "/var/packages/Tailscale/var/tailscaled.sock"
|
||||
if fi, err := os.Stat(dsm6Sock); err == nil && !fi.IsDir() {
|
||||
return dsm6Sock
|
||||
}
|
||||
if fi, err := os.Stat(dsm7Sock); err == nil && !fi.IsDir() {
|
||||
return dsm7Sock
|
||||
}
|
||||
}
|
||||
if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() {
|
||||
|
||||
@@ -1026,11 +1026,16 @@ func (k MachineKey) MarshalText() ([]byte, error) { return keyMarshalText("m
|
||||
func (k MachineKey) HexString() string { return fmt.Sprintf("%x", k[:]) }
|
||||
func (k *MachineKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "mkey:", text) }
|
||||
|
||||
func keyMarshalText(prefix string, k [32]byte) []byte {
|
||||
buf := make([]byte, len(prefix)+64)
|
||||
func appendKey(base []byte, prefix string, k [32]byte) []byte {
|
||||
ret := append(base, make([]byte, len(prefix)+64)...)
|
||||
buf := ret[len(base):]
|
||||
copy(buf, prefix)
|
||||
hex.Encode(buf[len(prefix):], k[:])
|
||||
return buf
|
||||
return ret
|
||||
}
|
||||
|
||||
func keyMarshalText(prefix string, k [32]byte) []byte {
|
||||
return appendKey(nil, prefix, k)
|
||||
}
|
||||
|
||||
func keyUnmarshalText(dst []byte, prefix string, text []byte) error {
|
||||
@@ -1061,6 +1066,7 @@ func (k DiscoKey) String() string { return fmt.Sprintf("discok
|
||||
func (k DiscoKey) MarshalText() ([]byte, error) { return keyMarshalText("discokey:", k), nil }
|
||||
func (k *DiscoKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "discokey:", text) }
|
||||
func (k DiscoKey) ShortString() string { return fmt.Sprintf("d:%x", k[:8]) }
|
||||
func (k DiscoKey) AppendTo(b []byte) []byte { return appendKey(b, "discokey:", k) }
|
||||
|
||||
// IsZero reports whether k is the zero value.
|
||||
func (k DiscoKey) IsZero() bool { return k == DiscoKey{} }
|
||||
@@ -1168,3 +1174,31 @@ const (
|
||||
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
|
||||
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
|
||||
)
|
||||
|
||||
// SetDNSRequest is a request to add a DNS record.
|
||||
//
|
||||
// This is used for ACME DNS-01 challenges (so people can use
|
||||
// LetsEncrypt, etc).
|
||||
//
|
||||
// The request is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box,
|
||||
// using the local machine key, and sent to:
|
||||
// https://login.tailscale.com/machine/<mkey hex>/set-dns
|
||||
type SetDNSRequest struct {
|
||||
// Version indicates what level of SetDNSRequest functionality
|
||||
// the client understands. Currently this type only has
|
||||
// one version; this field should always be 1 for now.
|
||||
Version int
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey NodeKey
|
||||
|
||||
// Name is the domain name for which to create a record.
|
||||
Name string
|
||||
|
||||
// Type is the DNS record type. For ACME DNS-01 challenges, it
|
||||
// should be "TXT".
|
||||
Type string
|
||||
|
||||
// Value is the value to add.
|
||||
Value string
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
@@ -528,3 +529,25 @@ func BenchmarkKeyMarshalText(b *testing.B) {
|
||||
sinkBytes = keyMarshalText("prefix", k)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendKeyAllocs(t *testing.T) {
|
||||
if version.IsRace() {
|
||||
t.Skip("skipping in race detector") // append(b, make([]byte, N)...) not optimized in compiler with race
|
||||
}
|
||||
var k [32]byte
|
||||
n := int(testing.AllocsPerRun(1000, func() {
|
||||
sinkBytes = keyMarshalText("prefix", k)
|
||||
}))
|
||||
if n != 1 {
|
||||
t.Fatalf("allocs = %v; want 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoKeyAppend(t *testing.T) {
|
||||
d := DiscoKey{1: 1, 2: 2}
|
||||
got := string(d.AppendTo([]byte("foo")))
|
||||
want := "foodiscokey:0001020000000000000000000000000000000000000000000000000000000000"
|
||||
if got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
100
tstest/integration/integration.go
Normal file
100
tstest/integration/integration.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// 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 integration contains Tailscale integration tests.
|
||||
//
|
||||
// This package is considered internal and the public API is subject
|
||||
// to change without notice.
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// Binaries are the paths to a tailscaled and tailscale binary.
|
||||
// These can be shared by multiple nodes.
|
||||
type Binaries struct {
|
||||
Dir string // temp dir for tailscale & tailscaled
|
||||
Daemon string // tailscaled
|
||||
CLI string // tailscale
|
||||
}
|
||||
|
||||
// BuildTestBinaries builds tailscale and tailscaled, failing the test
|
||||
// if they fail to compile.
|
||||
func BuildTestBinaries(t testing.TB) *Binaries {
|
||||
td := t.TempDir()
|
||||
build(t, td, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
|
||||
return &Binaries{
|
||||
Dir: td,
|
||||
Daemon: filepath.Join(td, "tailscaled"+exe()),
|
||||
CLI: filepath.Join(td, "tailscale"+exe()),
|
||||
}
|
||||
}
|
||||
|
||||
// buildMu limits our use of "go build" to one at a time, so we don't
|
||||
// fight Go's built-in caching trying to do the same build concurrently.
|
||||
var buildMu sync.Mutex
|
||||
|
||||
func build(t testing.TB, outDir string, targets ...string) {
|
||||
buildMu.Lock()
|
||||
defer buildMu.Unlock()
|
||||
|
||||
t0 := time.Now()
|
||||
defer func() { t.Logf("built %s in %v", targets, time.Since(t0).Round(time.Millisecond)) }()
|
||||
|
||||
goBin := findGo(t)
|
||||
cmd := exec.Command(goBin, "install")
|
||||
if version.IsRace() {
|
||||
cmd.Args = append(cmd.Args, "-race")
|
||||
}
|
||||
cmd.Args = append(cmd.Args, targets...)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOBIN="+outDir)
|
||||
errOut, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if strings.Contains(string(errOut), "when GOBIN is set") {
|
||||
// Fallback slow path for cross-compiled binaries.
|
||||
for _, target := range targets {
|
||||
outFile := filepath.Join(outDir, path.Base(target)+exe())
|
||||
cmd := exec.Command(goBin, "build", "-o", outFile, target)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
|
||||
if errOut, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
|
||||
}
|
||||
|
||||
func findGo(t testing.TB) string {
|
||||
goBin := filepath.Join(runtime.GOROOT(), "bin", "go"+exe())
|
||||
if fi, err := os.Stat(goBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Fatalf("failed to find go at %v", goBin)
|
||||
}
|
||||
t.Fatalf("looking for go binary: %v", err)
|
||||
} else if !fi.Mode().IsRegular() {
|
||||
t.Fatalf("%v is unexpected %v", goBin, fi.Mode())
|
||||
}
|
||||
return goBin
|
||||
}
|
||||
|
||||
func exe() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package integration contains Tailscale integration tests.
|
||||
package integration
|
||||
|
||||
import (
|
||||
@@ -21,7 +20,6 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -44,7 +42,6 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var verbose = flag.Bool("verbose", false, "verbose debug logs")
|
||||
@@ -65,7 +62,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func TestOneNodeUp_NoAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
@@ -107,7 +104,7 @@ func TestOneNodeUp_NoAuth(t *testing.T) {
|
||||
|
||||
func TestOneNodeUp_Auth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
@@ -154,7 +151,7 @@ func TestOneNodeUp_Auth(t *testing.T) {
|
||||
|
||||
func TestTwoNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
@@ -196,23 +193,88 @@ func TestTwoNodes(t *testing.T) {
|
||||
d2.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
// testBinaries are the paths to a tailscaled and tailscale binary.
|
||||
// These can be shared by multiple nodes.
|
||||
type testBinaries struct {
|
||||
dir string // temp dir for tailscale & tailscaled
|
||||
daemon string // tailscaled
|
||||
cli string // tailscale
|
||||
func TestNodeAddressIPFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n1.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
|
||||
testNodes := env.Control.AllNodes()
|
||||
|
||||
if len(testNodes) != 1 {
|
||||
t.Errorf("Expected %d nodes, got %d", 1, len(testNodes))
|
||||
}
|
||||
node := testNodes[0]
|
||||
if len(node.Addresses) == 0 {
|
||||
t.Errorf("Empty Addresses field in node")
|
||||
}
|
||||
if len(node.AllowedIPs) == 0 {
|
||||
t.Errorf("Empty AllowedIPs field in node")
|
||||
}
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
// buildTestBinaries builds tailscale and tailscaled, failing the test
|
||||
// if they fail to compile.
|
||||
func buildTestBinaries(t testing.TB) *testBinaries {
|
||||
td := t.TempDir()
|
||||
build(t, td, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
|
||||
return &testBinaries{
|
||||
dir: td,
|
||||
daemon: filepath.Join(td, "tailscaled"+exe()),
|
||||
cli: filepath.Join(td, "tailscale"+exe()),
|
||||
func TestAddPingRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n1.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
|
||||
gotPing := make(chan bool, 1)
|
||||
waitPing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPing <- true
|
||||
}))
|
||||
defer waitPing.Close()
|
||||
|
||||
nodes := env.Control.AllNodes()
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("expected 1 node, got %d nodes", len(nodes))
|
||||
}
|
||||
|
||||
nodeKey := nodes[0].Key
|
||||
pr := &tailcfg.PingRequest{URL: waitPing.URL, Log: true}
|
||||
ok := env.Control.AddPingRequest(nodeKey, pr)
|
||||
if !ok {
|
||||
t.Fatalf("no node found with NodeKey %v in AddPingRequest", nodeKey)
|
||||
}
|
||||
|
||||
// Wait for PingRequest to come back
|
||||
waitDuration := 10 * time.Second
|
||||
pingTimeout := time.NewTimer(waitDuration)
|
||||
|
||||
// Ticker sends new PingRequests if the previous did not get through
|
||||
ticker := time.NewTicker(waitDuration / 5)
|
||||
|
||||
select {
|
||||
case <-gotPing:
|
||||
pingTimeout.Stop()
|
||||
ticker.Stop()
|
||||
case <-ticker.C:
|
||||
ok := env.Control.AddPingRequest(nodeKey, pr)
|
||||
if !ok {
|
||||
t.Fatalf("no node found with NodeKey %v in AddPingRequest", nodeKey)
|
||||
}
|
||||
case <-pingTimeout.C:
|
||||
t.Error("didn't get PingRequest from tailscaled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +282,7 @@ func buildTestBinaries(t testing.TB) *testBinaries {
|
||||
// or more nodes.
|
||||
type testEnv struct {
|
||||
t testing.TB
|
||||
Binaries *testBinaries
|
||||
Binaries *Binaries
|
||||
|
||||
LogCatcher *logCatcher
|
||||
LogCatcherServer *httptest.Server
|
||||
@@ -238,7 +300,7 @@ type testEnv struct {
|
||||
// environment.
|
||||
//
|
||||
// Call Close to shut everything down.
|
||||
func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
|
||||
func newTestEnv(t testing.TB, bins *Binaries) *testEnv {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("not tested/working on Windows yet")
|
||||
}
|
||||
@@ -321,7 +383,7 @@ func (d *Daemon) MustCleanShutdown(t testing.TB) {
|
||||
// StartDaemon starts the node's tailscaled, failing if it fails to
|
||||
// start.
|
||||
func (n *testNode) StartDaemon(t testing.TB) *Daemon {
|
||||
cmd := exec.Command(n.env.Binaries.daemon,
|
||||
cmd := exec.Command(n.env.Binaries.Daemon,
|
||||
"--tun=userspace-networking",
|
||||
"--state="+n.stateFile,
|
||||
"--socket="+n.sockFile,
|
||||
@@ -399,7 +461,7 @@ func (n *testNode) AwaitRunning(t testing.TB) {
|
||||
// Tailscale returns a command that runs the tailscale CLI with the provided arguments.
|
||||
// It does not start the process.
|
||||
func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
|
||||
cmd := exec.Command(n.env.Binaries.cli, "--socket="+n.sockFile)
|
||||
cmd := exec.Command(n.env.Binaries.CLI, "--socket="+n.sockFile)
|
||||
cmd.Args = append(cmd.Args, arg...)
|
||||
cmd.Dir = n.dir
|
||||
return cmd
|
||||
@@ -426,63 +488,6 @@ func (n *testNode) MustStatus(tb testing.TB) *ipnstate.Status {
|
||||
return st
|
||||
}
|
||||
|
||||
func exe() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findGo(t testing.TB) string {
|
||||
goBin := filepath.Join(runtime.GOROOT(), "bin", "go"+exe())
|
||||
if fi, err := os.Stat(goBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Fatalf("failed to find go at %v", goBin)
|
||||
}
|
||||
t.Fatalf("looking for go binary: %v", err)
|
||||
} else if !fi.Mode().IsRegular() {
|
||||
t.Fatalf("%v is unexpected %v", goBin, fi.Mode())
|
||||
}
|
||||
return goBin
|
||||
}
|
||||
|
||||
// buildMu limits our use of "go build" to one at a time, so we don't
|
||||
// fight Go's built-in caching trying to do the same build concurrently.
|
||||
var buildMu sync.Mutex
|
||||
|
||||
func build(t testing.TB, outDir string, targets ...string) {
|
||||
buildMu.Lock()
|
||||
defer buildMu.Unlock()
|
||||
|
||||
t0 := time.Now()
|
||||
defer func() { t.Logf("built %s in %v", targets, time.Since(t0).Round(time.Millisecond)) }()
|
||||
|
||||
goBin := findGo(t)
|
||||
cmd := exec.Command(goBin, "install")
|
||||
if version.IsRace() {
|
||||
cmd.Args = append(cmd.Args, "-race")
|
||||
}
|
||||
cmd.Args = append(cmd.Args, targets...)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOBIN="+outDir)
|
||||
errOut, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if strings.Contains(string(errOut), "when GOBIN is set") {
|
||||
// Fallback slow path for cross-compiled binaries.
|
||||
for _, target := range targets {
|
||||
outFile := filepath.Join(outDir, path.Base(target)+exe())
|
||||
cmd := exec.Command(goBin, "build", "-o", outFile, target)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
|
||||
if errOut, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
|
||||
}
|
||||
|
||||
// logCatcher is a minimal logcatcher for the logtail upload client.
|
||||
type logCatcher struct {
|
||||
mu sync.Mutex
|
||||
|
||||
@@ -54,6 +54,39 @@ type Server struct {
|
||||
updates map[tailcfg.NodeID]chan updateType
|
||||
authPath map[string]*AuthPath
|
||||
nodeKeyAuthed map[tailcfg.NodeKey]bool // key => true once authenticated
|
||||
pingReqsToAdd map[tailcfg.NodeKey]*tailcfg.PingRequest
|
||||
}
|
||||
|
||||
// NumNodes returns the number of nodes in the testcontrol server.
|
||||
//
|
||||
// This is useful when connecting a bunch of virtual machines to a testcontrol
|
||||
// server to see how many of them connected successfully.
|
||||
func (s *Server) NumNodes() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return len(s.nodes)
|
||||
}
|
||||
|
||||
// AddPingRequest sends the ping pr to nodeKeyDst. It reports whether it did so. That is,
|
||||
// it reports whether nodeKeyDst was connected.
|
||||
func (s *Server) AddPingRequest(nodeKeyDst tailcfg.NodeKey, pr *tailcfg.PingRequest) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.pingReqsToAdd == nil {
|
||||
s.pingReqsToAdd = map[tailcfg.NodeKey]*tailcfg.PingRequest{}
|
||||
}
|
||||
// Now send the update to the channel
|
||||
node := s.nodeLocked(nodeKeyDst)
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
s.pingReqsToAdd[nodeKeyDst] = pr
|
||||
nodeID := node.ID
|
||||
oldUpdatesCh := s.updates[nodeID]
|
||||
sendUpdate(oldUpdatesCh, updateDebugInjection)
|
||||
return true
|
||||
}
|
||||
|
||||
type AuthPath struct {
|
||||
@@ -165,6 +198,13 @@ func (s *Server) serveMachine(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) Node(nodeKey tailcfg.NodeKey) *tailcfg.Node {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.nodeLocked(nodeKey)
|
||||
}
|
||||
|
||||
// nodeLocked returns the node for nodeKey. It's always nil or cloned memory.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *Server) nodeLocked(nodeKey tailcfg.NodeKey) *tailcfg.Node {
|
||||
return s.nodes[nodeKey].Clone()
|
||||
}
|
||||
|
||||
@@ -307,6 +347,10 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tail
|
||||
|
||||
machineAuthorized := true // TODO: add Server.RequireMachineAuth
|
||||
|
||||
allowedIPs := []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix(fmt.Sprintf("100.64.%d.%d/32", uint8(tailcfg.NodeID(user.ID)>>8), uint8(tailcfg.NodeID(user.ID)))),
|
||||
}
|
||||
|
||||
s.nodes[req.NodeKey] = &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(user.ID),
|
||||
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(user.ID))),
|
||||
@@ -314,6 +358,8 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tail
|
||||
Machine: mkey,
|
||||
Key: req.NodeKey,
|
||||
MachineAuthorized: machineAuthorized,
|
||||
Addresses: allowedIPs,
|
||||
AllowedIPs: allowedIPs,
|
||||
}
|
||||
requireAuth := s.RequireAuth
|
||||
if requireAuth && s.nodeKeyAuthed[req.NodeKey] {
|
||||
@@ -356,6 +402,9 @@ const (
|
||||
// via a lite endpoint update. These ones are never dup-suppressed,
|
||||
// as the client is expecting an answer regardless.
|
||||
updateSelfChanged
|
||||
|
||||
// updateDebugInjection is an update used for PingRequests
|
||||
updateDebugInjection
|
||||
)
|
||||
|
||||
func (s *Server) updateLocked(source string, peers []tailcfg.NodeID) {
|
||||
@@ -537,6 +586,14 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
|
||||
netaddr.MustParseIPPrefix(fmt.Sprintf("100.64.%d.%d/32", uint8(node.ID>>8), uint8(node.ID))),
|
||||
}
|
||||
res.Node.AllowedIPs = res.Node.Addresses
|
||||
|
||||
// Consume the PingRequest while protected by mutex if it exists
|
||||
s.mu.Lock()
|
||||
if pr, ok := s.pingReqsToAdd[node.Key]; ok {
|
||||
res.PingRequest = pr
|
||||
delete(s.pingReqsToAdd, node.Key)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
|
||||
98
tstest/integration/vms/README.md
Normal file
98
tstest/integration/vms/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# End-to-End VM-based Integration Testing
|
||||
|
||||
This test spins up a bunch of common linux distributions and then tries to get
|
||||
them to connect to a
|
||||
[`testcontrol`](https://pkg.go.dev/tailscale.com/tstest/integration/testcontrol)
|
||||
server.
|
||||
|
||||
## Running
|
||||
|
||||
This test currently only runs on Linux.
|
||||
|
||||
This test depends on the following command line tools:
|
||||
|
||||
- [qemu](https://www.qemu.org/)
|
||||
- [cdrkit](https://en.wikipedia.org/wiki/Cdrkit)
|
||||
- [openssh](https://www.openssh.com/)
|
||||
|
||||
This test also requires the following:
|
||||
|
||||
- about 10 GB of temporary storage
|
||||
- about 10 GB of cached VM images
|
||||
- at least 4 GB of ram for virtual machines
|
||||
- hardware virtualization support
|
||||
([KVM](https://www.linux-kvm.org/page/Main_Page)) enabled in the BIOS
|
||||
- the `kvm` module to be loaded (`modprobe kvm`)
|
||||
- the user running these tests must have access to `/dev/kvm` (being in the
|
||||
`kvm` group should suffice)
|
||||
|
||||
This optionally requires an AWS profile to be configured at the [default
|
||||
path](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html).
|
||||
The S3 bucket is set so that the requester pays. Please keep this in mind when
|
||||
running these tests on your machine. If you are uncomfortable with the cost from
|
||||
downloading from S3, you should pass the `-no-s3` flag to disable downloads from
|
||||
S3. However keep in mind that some distributions do not use stable URLs for each
|
||||
individual image artifact, so there may be spurious test failures as a result.
|
||||
|
||||
If you are using [Nix](https://nixos.org), you can run all of the tests with the
|
||||
correct command line tools using this command:
|
||||
|
||||
```console
|
||||
$ nix-shell -p openssh -p go -p qemu -p cdrkit --run "go test . --run-vm-tests --v --timeout 30m"
|
||||
```
|
||||
|
||||
Keep the timeout high for the first run, especially if you are not downloading
|
||||
VM images from S3. The mirrors we pull images from have download rate limits and
|
||||
will take a while to download.
|
||||
|
||||
Because of the hardware requirements of this test, this test will not run
|
||||
without the `--run-vm-tests` flag set.
|
||||
|
||||
## Other Fun Flags
|
||||
|
||||
This test's behavior is customized with command line flags.
|
||||
|
||||
### Don't Download Images From S3
|
||||
|
||||
If you pass the `-no-s3` flag to `go test`, the S3 step will be skipped in favor
|
||||
of downloading the images directly from upstream sources, which may cause the
|
||||
test to fail in odd places.
|
||||
|
||||
### Distribution Picking
|
||||
|
||||
This test runs on a large number of distributions. By default it tries to run
|
||||
everything, which may or may not be ideal for you. If you only want to test a
|
||||
subset of distributions, you can use the `--distro-regex` flag to match a subset
|
||||
of distributions using a [regular expression](https://golang.org/pkg/regexp/)
|
||||
such as like this:
|
||||
|
||||
```console
|
||||
$ go test -run-vm-tests -distro-regex centos
|
||||
```
|
||||
|
||||
This would run all tests on all versions of CentOS.
|
||||
|
||||
```console
|
||||
$ go test -run-vm-tests -distro-regex '(debian|ubuntu)'
|
||||
```
|
||||
|
||||
This would run all tests on all versions of Debian and Ubuntu.
|
||||
|
||||
### Ram Limiting
|
||||
|
||||
This test uses a lot of memory. In order to avoid making machines run out of
|
||||
memory running this test, a semaphore is used to limit how many megabytes of ram
|
||||
are being used at once. By default this semaphore is set to 4096 MB of ram
|
||||
(about 4 gigabytes). You can customize this with the `--ram-limit` flag:
|
||||
|
||||
```console
|
||||
$ go test --run-vm-tests --ram-limit 2048
|
||||
$ go test --run-vm-tests --ram-limit 65536
|
||||
```
|
||||
|
||||
The first example will set the limit to 2048 MB of ram (about 2 gigabytes). The
|
||||
second example will set the limit to 65536 MB of ram (about 65 gigabytes).
|
||||
Please be careful with this flag, improper usage of it is known to cause the
|
||||
Linux out-of-memory killer to engage. Try to keep it within 50-75% of your
|
||||
machine's available ram (there is some overhead involved with the
|
||||
virtualization) to be on the safe side.
|
||||
7
tstest/integration/vms/doc.go
Normal file
7
tstest/integration/vms/doc.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// 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 vms does VM-based integration/functional tests by using
|
||||
// qemu and a bank of pre-made VM images.
|
||||
package vms
|
||||
86
tstest/integration/vms/opensuse_leap_15_1_test.go
Normal file
86
tstest/integration/vms/opensuse_leap_15_1_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package vms
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
/*
|
||||
The images that we use for OpenSUSE Leap 15.1 have an issue that makes the
|
||||
nocloud backend[1] for cloud-init just not work. As a distro-specific
|
||||
workaround, we're gonna pretend to be OpenStack.
|
||||
|
||||
TODO(Xe): delete once we no longer need to support OpenSUSE Leap 15.1.
|
||||
|
||||
[1]: https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html
|
||||
*/
|
||||
|
||||
type openSUSELeap151MetaData struct {
|
||||
Zone string `json:"availability_zone"` // nova
|
||||
Hostname string `json:"hostname"` // opensuse-leap-15-1
|
||||
LaunchIndex string `json:"launch_index"` // 0
|
||||
Meta openSUSELeap151MetaDataMeta `json:"meta"` // some openstack metadata we don't need to care about
|
||||
Name string `json:"name"` // opensuse-leap-15-1
|
||||
UUID string `json:"uuid"` // e9c664cd-b116-433b-aa61-7ff420163dcd
|
||||
}
|
||||
|
||||
type openSUSELeap151MetaDataMeta struct {
|
||||
Role string `json:"role"` // server
|
||||
DSMode string `json:"dsmode"` // local
|
||||
Essential string `json:"essential"` // essential
|
||||
}
|
||||
|
||||
func hackOpenSUSE151UserData(t *testing.T, d Distro, dir string) bool {
|
||||
if d.name != "opensuse-leap-15-1" {
|
||||
return false
|
||||
}
|
||||
|
||||
t.Log("doing OpenSUSE Leap 15.1 hack")
|
||||
osDir := filepath.Join(dir, "openstack", "latest")
|
||||
err := os.MkdirAll(osDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make metadata home: %v", err)
|
||||
}
|
||||
|
||||
metadata, err := json.Marshal(openSUSELeap151MetaData{
|
||||
Zone: "nova",
|
||||
Hostname: d.name,
|
||||
LaunchIndex: "0",
|
||||
Meta: openSUSELeap151MetaDataMeta{
|
||||
Role: "server",
|
||||
DSMode: "local",
|
||||
Essential: "false",
|
||||
},
|
||||
Name: d.name,
|
||||
UUID: uuid.New().String(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("can't encode metadata: %v", err)
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(osDir, "meta_data.json"), metadata, 0666)
|
||||
if err != nil {
|
||||
t.Fatalf("can't write to meta_data.json: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dir, "user-data"))
|
||||
if err != nil {
|
||||
t.Fatalf("can't read user_data: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(osDir, "user_data"), data, 0666)
|
||||
if err != nil {
|
||||
t.Fatalf("can't create output user_data: %v", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
30
tstest/integration/vms/regex_flag.go
Normal file
30
tstest/integration/vms/regex_flag.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package vms
|
||||
|
||||
import "regexp"
|
||||
|
||||
type regexValue struct {
|
||||
r *regexp.Regexp
|
||||
}
|
||||
|
||||
func (r *regexValue) String() string {
|
||||
if r.r == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return r.r.String()
|
||||
}
|
||||
|
||||
func (r *regexValue) Set(val string) error {
|
||||
if rex, err := regexp.Compile(val); err != nil {
|
||||
return err
|
||||
} else {
|
||||
r.r = rex
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r regexValue) Unwrap() *regexp.Regexp { return r.r }
|
||||
22
tstest/integration/vms/regex_flag_test.go
Normal file
22
tstest/integration/vms/regex_flag_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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 vms
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegexFlag(t *testing.T) {
|
||||
var v regexValue
|
||||
fs := flag.NewFlagSet(t.Name(), flag.PanicOnError)
|
||||
fs.Var(&v, "regex", "regex to parse")
|
||||
|
||||
const want = `.*`
|
||||
fs.Parse([]string{"-regex", want})
|
||||
if v.Unwrap().String() != want {
|
||||
t.Fatalf("got wrong regex: %q, wanted: %q", v.Unwrap().String(), want)
|
||||
}
|
||||
}
|
||||
77
tstest/integration/vms/runner.nix
Normal file
77
tstest/integration/vms/runner.nix
Normal file
@@ -0,0 +1,77 @@
|
||||
# This is a NixOS module to allow a machine to act as an integration test
|
||||
# runner. This is used for the end-to-end VM test suite.
|
||||
|
||||
{ lib, config, pkgs, ... }:
|
||||
|
||||
{
|
||||
# The GitHub Actions self-hosted runner service.
|
||||
services.github-runner = {
|
||||
enable = true;
|
||||
url = "https://github.com/tailscale/tailscale";
|
||||
replace = true;
|
||||
extraLabels = [ "vm_integration_test" ];
|
||||
|
||||
# Justifications for the packages:
|
||||
extraPackages = with pkgs; [
|
||||
# The test suite is written in Go.
|
||||
go
|
||||
|
||||
# This contains genisoimage, which is needed to create cloud-init
|
||||
# seeds.
|
||||
cdrkit
|
||||
|
||||
# This package is the virtual machine hypervisor we use in tests.
|
||||
qemu
|
||||
|
||||
# This package contains tools like `ssh-keygen`.
|
||||
openssh
|
||||
|
||||
# The C complier so cgo builds work.
|
||||
gcc
|
||||
];
|
||||
|
||||
# Customize this to include your GitHub username so we can track
|
||||
# who is running which node.
|
||||
name = "YOUR-GITHUB-USERNAME-tstest-integration-vms";
|
||||
|
||||
# Replace this with the path to the GitHub Actions runner token on
|
||||
# your disk.
|
||||
tokenFile = "/run/decrypted/ts-oss-ghaction-token";
|
||||
};
|
||||
|
||||
# A user account so there is a home directory and so they have kvm
|
||||
# access. Please don't change this account name.
|
||||
users.users.ghrunner = {
|
||||
createHome = true;
|
||||
isSystemUser = true;
|
||||
extraGroups = [ "kvm" ];
|
||||
};
|
||||
|
||||
# The default github-runner service sets a lot of isolation features
|
||||
# that attempt to limit the damage that malicious code can use.
|
||||
# Unfortunately we rely on some "dangerous" features to do these tests,
|
||||
# so this shim will peel some of them away.
|
||||
systemd.services.github-runner = {
|
||||
serviceConfig = {
|
||||
# We need access to /dev to poke /dev/kvm.
|
||||
PrivateDevices = lib.mkForce false;
|
||||
|
||||
# /dev/kvm is how qemu creates a virtual machine with KVM.
|
||||
DeviceAllow = lib.mkForce [ "/dev/kvm" ];
|
||||
|
||||
# Ensure the service has KVM permissions with the `kvm` group.
|
||||
ExtraGroups = [ "kvm" ];
|
||||
|
||||
# The service runs as a dynamic user by default. This makes it hard
|
||||
# to persistently store things in /var/lib/ghrunner. This line
|
||||
# disables the dynamic user feature.
|
||||
DynamicUser = lib.mkForce false;
|
||||
|
||||
# Run this service as our ghrunner user.
|
||||
User = "ghrunner";
|
||||
|
||||
# We need access to /var/lib/ghrunner to store VM images.
|
||||
ProtectSystem = lib.mkForce null;
|
||||
};
|
||||
};
|
||||
}
|
||||
805
tstest/integration/vms/vms_test.go
Normal file
805
tstest/integration/vms/vms_test.go
Normal file
@@ -0,0 +1,805 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package vms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
expect "github.com/google/goexpect"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
)
|
||||
|
||||
const (
|
||||
securePassword = "hunter2"
|
||||
bucketName = "tailscale-integration-vm-images"
|
||||
)
|
||||
|
||||
var (
|
||||
runVMTests = flag.Bool("run-vm-tests", false, "if set, run expensive VM based integration tests")
|
||||
noS3 = flag.Bool("no-s3", false, "if set, always download images from the public internet (risks breaking)")
|
||||
vmRamLimit = flag.Int("ram-limit", 4096, "the maximum number of megabytes of ram that can be used for VMs, must be greater than or equal to 1024")
|
||||
distroRex = func() *regexValue {
|
||||
result := ®exValue{r: regexp.MustCompile(`.*`)}
|
||||
flag.Var(result, "distro-regex", "The regex that matches what distros should be run")
|
||||
return result
|
||||
}()
|
||||
)
|
||||
|
||||
type Distro struct {
|
||||
name string // amazon-linux
|
||||
url string // URL to a qcow2 image
|
||||
sha256sum string // hex-encoded sha256 sum of contents of URL
|
||||
mem int // VM memory in megabytes
|
||||
packageManager string // yum/apt/dnf/zypper
|
||||
}
|
||||
|
||||
func (d *Distro) InstallPre() string {
|
||||
switch d.packageManager {
|
||||
case "yum":
|
||||
return ` - [ yum, update, gnupg2 ]
|
||||
- [ yum, "-y", install, iptables ]`
|
||||
case "zypper":
|
||||
return ` - [ zypper, in, "-y", iptables ]`
|
||||
|
||||
case "dnf":
|
||||
return ` - [ dnf, install, "-y", iptables ]`
|
||||
|
||||
case "apt":
|
||||
return ` - [ apt-get, update ]
|
||||
- [ apt-get, "-y", install, curl, "apt-transport-https", gnupg2 ]`
|
||||
|
||||
case "apk":
|
||||
return ` - [ apk, "-U", add, curl, "ca-certificates" ]`
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestDownloadImages(t *testing.T) {
|
||||
if !*runVMTests {
|
||||
t.Skip("not running integration tests (need --run-vm-tests)")
|
||||
}
|
||||
|
||||
for _, d := range distros {
|
||||
distro := d
|
||||
t.Run(distro.name, func(t *testing.T) {
|
||||
if !distroRex.Unwrap().MatchString(distro.name) {
|
||||
t.Skipf("distro name %q doesn't match regex: %s", distro.name, distroRex)
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
fetchDistro(t, distro)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var distros = []Distro{
|
||||
// NOTE(Xe): If you run into issues getting the autoconfig to work, comment
|
||||
// out all the other distros and uncomment this one. Connect with a VNC
|
||||
// client with a command like this:
|
||||
//
|
||||
// $ vncviewer :0
|
||||
//
|
||||
// On NixOS you can get away with something like this:
|
||||
//
|
||||
// $ env NIXPKGS_ALLOW_UNFREE=1 nix-shell -p tigervnc --run 'vncviewer :0'
|
||||
//
|
||||
// Login as root with the password root. Then look in
|
||||
// /var/log/cloud-init-output.log for what you messed up.
|
||||
|
||||
// {"alpine-edge", "https://xena.greedo.xeserv.us/pkg/alpine/img/alpine-edge-2021-05-18-cloud-init-within.qcow2", "b3bb15311c0bd3beffa1b554f022b75d3b7309b5fdf76fb146fe7c72b83b16d0", 256, "apk"},
|
||||
|
||||
{"amazon-linux", "https://cdn.amazonlinux.com/os-images/2.0.20210427.0/kvm/amzn2-kvm-2.0.20210427.0-x86_64.xfs.gpt.qcow2", "6ef9daef32cec69b2d0088626ec96410cd24afc504d57278bbf2f2ba2b7e529b", 512, "yum"},
|
||||
{"arch", "https://mirror.pkgbuild.com/images/v20210515.22945/Arch-Linux-x86_64-cloudimg-20210515.22945.qcow2", "e4077f5ba3c5d545478f64834bc4852f9f7a2e05950fce8ecd0df84193162a27", 512, "pacman"},
|
||||
{"centos-7", "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2003.qcow2c", "b7555ecf90b24111f2efbc03c1e80f7b38f1e1fc7e1b15d8fee277d1a4575e87", 512, "yum"},
|
||||
{"centos-8", "https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.3.2011-20201204.2.x86_64.qcow2", "7ec97062618dc0a7ebf211864abf63629da1f325578868579ee70c495bed3ba0", 768, "dnf"},
|
||||
{"debian-9", "http://cloud.debian.org/images/cloud/OpenStack/9.13.22-20210531/debian-9.13.22-20210531-openstack-amd64.qcow2", "c36e25f2ab0b5be722180db42ed9928476812f02d053620e1c287f983e9f6f1d", 512, "apt"},
|
||||
{"debian-10", "https://cdimage.debian.org/images/cloud/buster/20210329-591/debian-10-generic-amd64-20210329-591.qcow2", "70c61956095870c4082103d1a7a1cb5925293f8405fc6cb348588ec97e8611b0", 768, "apt"},
|
||||
{"fedora-34", "https://download.fedoraproject.org/pub/fedora/linux/releases/34/Cloud/x86_64/images/Fedora-Cloud-Base-34-1.2.x86_64.qcow2", "b9b621b26725ba95442d9a56cbaa054784e0779a9522ec6eafff07c6e6f717ea", 768, "dnf"},
|
||||
{"opensuse-leap-15-1", "https://download.opensuse.org/repositories/Cloud:/Images:/Leap_15.1/images/openSUSE-Leap-15.1-OpenStack.x86_64.qcow2", "40bc72b8ee143364fc401f2c9c9a11ecb7341a29fa84c6f7bf42fc94acf19a02", 512, "zypper"},
|
||||
{"opensuse-leap-15-2", "https://download.opensuse.org/repositories/Cloud:/Images:/Leap_15.2/images/openSUSE-Leap-15.2-OpenStack.x86_64.qcow2", "4df9cee9281d1f57d20f79dc65d76e255592b904760e73c0dd44ac753a54330f", 512, "zypper"},
|
||||
{"opensuse-leap-15-3", "http://mirror.its.dal.ca/opensuse/distribution/leap/15.3/appliances/openSUSE-Leap-15.3-JeOS.x86_64-OpenStack-Cloud.qcow2", "22e0392e4d0becb523d1bc5f709366140b7ee20d6faf26de3d0f9046d1ee15d5", 512, "zypper"},
|
||||
{"opensuse-tumbleweed", "https://download.opensuse.org/tumbleweed/appliances/openSUSE-Tumbleweed-JeOS.x86_64-OpenStack-Cloud.qcow2", "79e610bba3ed116556608f031c06e4b9260e3be2b193ce1727914ba213afac3f", 512, "zypper"},
|
||||
{"ubuntu-16-04", "https://cloud-images.ubuntu.com/xenial/20210429/xenial-server-cloudimg-amd64-disk1.img", "50a21bc067c05e0c73bf5d8727ab61152340d93073b3dc32eff18b626f7d813b", 512, "apt"},
|
||||
{"ubuntu-18-04", "https://cloud-images.ubuntu.com/bionic/20210526/bionic-server-cloudimg-amd64.img", "389ffd5d36bbc7a11bf384fd217cda9388ccae20e5b0cb7d4516733623c96022", 512, "apt"},
|
||||
{"ubuntu-20-04", "https://cloud-images.ubuntu.com/focal/20210603/focal-server-cloudimg-amd64.img", "1c0969323b058ba8b91fec245527069c2f0502fc119b9138b213b6bfebd965cb", 512, "apt"},
|
||||
{"ubuntu-20-10", "https://cloud-images.ubuntu.com/groovy/20210604/groovy-server-cloudimg-amd64.img", "2196df5f153faf96443e5502bfdbcaa0baaefbaec614348fec344a241855b0ef", 512, "apt"},
|
||||
{"ubuntu-21-04", "https://cloud-images.ubuntu.com/hirsute/20210603/hirsute-server-cloudimg-amd64.img", "bf07f36fc99ff521d3426e7d257e28f0c81feebc9780b0c4f4e25ae594ff4d3b", 512, "apt"},
|
||||
}
|
||||
|
||||
// fetchFromS3 fetches a distribution image from Amazon S3 or reports whether
|
||||
// it is unable to. It can fail to fetch from S3 if there is either no AWS
|
||||
// configuration (in ~/.aws/credentials) or if the `-no-s3` flag is passed. In
|
||||
// that case the test will fall back to downloading distribution images from the
|
||||
// public internet.
|
||||
//
|
||||
// Like fetching from HTTP, the test will fail if an error is encountered during
|
||||
// the downloading process.
|
||||
//
|
||||
// This function writes the distribution image to fout. It is always closed. Do
|
||||
// not expect fout to remain writable.
|
||||
func fetchFromS3(t *testing.T, fout *os.File, d Distro) bool {
|
||||
t.Helper()
|
||||
|
||||
if *noS3 {
|
||||
t.Log("you asked to not use S3, not using S3")
|
||||
return false
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("can't make AWS session: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
dler := s3manager.NewDownloader(sess, func(d *s3manager.Downloader) {
|
||||
d.PartSize = 64 * 1024 * 1024 // 64MB per part
|
||||
})
|
||||
|
||||
t.Logf("fetching s3://%s/%s", bucketName, d.sha256sum)
|
||||
|
||||
_, err = dler.Download(fout, &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(d.sha256sum),
|
||||
})
|
||||
if err != nil {
|
||||
fout.Close()
|
||||
t.Fatalf("can't get s3://%s/%s: %v", bucketName, d.sha256sum, err)
|
||||
}
|
||||
|
||||
err = fout.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("can't close fout: %v", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// fetchDistro fetches a distribution from the internet if it doesn't already exist locally. It
|
||||
// also validates the sha256 sum from a known good hash.
|
||||
func fetchDistro(t *testing.T, resultDistro Distro) {
|
||||
t.Helper()
|
||||
|
||||
cdir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
t.Fatalf("can't find cache dir: %v", err)
|
||||
}
|
||||
cdir = filepath.Join(cdir, "tailscale", "vm-test")
|
||||
|
||||
qcowPath := filepath.Join(cdir, "qcow2", resultDistro.sha256sum)
|
||||
|
||||
_, err = os.Stat(qcowPath)
|
||||
if err == nil {
|
||||
hash := checkCachedImageHash(t, resultDistro, cdir)
|
||||
if hash != resultDistro.sha256sum {
|
||||
t.Logf("hash for %s (%s) doesn't match expected %s, re-downloading", resultDistro.name, qcowPath, resultDistro.sha256sum)
|
||||
err = errors.New("some fake non-nil error to force a redownload")
|
||||
|
||||
if err := os.Remove(qcowPath); err != nil {
|
||||
t.Fatalf("can't delete wrong cached image: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Logf("downloading distro image %s to %s", resultDistro.url, qcowPath)
|
||||
fout, err := os.Create(qcowPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !fetchFromS3(t, fout, resultDistro) {
|
||||
resp, err := http.Get(resultDistro.url)
|
||||
if err != nil {
|
||||
t.Fatalf("can't fetch qcow2 for %s (%s): %v", resultDistro.name, resultDistro.url, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
t.Fatalf("%s replied %s", resultDistro.url, resp.Status)
|
||||
}
|
||||
|
||||
_, err = io.Copy(fout, resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("download of %s failed: %v", resultDistro.url, err)
|
||||
}
|
||||
|
||||
hash := checkCachedImageHash(t, resultDistro, cdir)
|
||||
|
||||
if hash != resultDistro.sha256sum {
|
||||
t.Fatalf("hash mismatch, want: %s, got: %s", resultDistro.sha256sum, hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkCachedImageHash(t *testing.T, d Distro, cacheDir string) (gotHash string) {
|
||||
t.Helper()
|
||||
|
||||
qcowPath := filepath.Join(cacheDir, "qcow2", d.sha256sum)
|
||||
|
||||
fin, err := os.Open(qcowPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, fin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
if hash != d.sha256sum {
|
||||
t.Fatalf("hash mismatch, got: %q, want: %q", hash, d.sha256sum)
|
||||
}
|
||||
|
||||
gotHash = hash
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// run runs a command or fails the test.
|
||||
func run(t *testing.T, dir, prog string, args ...string) {
|
||||
t.Helper()
|
||||
t.Logf("running: %s %s", prog, strings.Join(args, " "))
|
||||
tstest.FixLogs(t)
|
||||
|
||||
cmd := exec.Command(prog, args...)
|
||||
cmd.Stdout = log.Writer()
|
||||
cmd.Stderr = log.Writer()
|
||||
cmd.Dir = dir
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// mkLayeredQcow makes a layered qcow image that allows us to keep the upstream
|
||||
// VM images pristine and only do our changes on an overlay.
|
||||
func mkLayeredQcow(t *testing.T, tdir string, d Distro) {
|
||||
t.Helper()
|
||||
|
||||
cdir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
t.Fatalf("can't find cache dir: %v", err)
|
||||
}
|
||||
cdir = filepath.Join(cdir, "tailscale", "vm-test")
|
||||
|
||||
run(t, tdir, "qemu-img", "create",
|
||||
"-f", "qcow2",
|
||||
"-o", "backing_file="+filepath.Join(cdir, "qcow2", d.sha256sum),
|
||||
filepath.Join(tdir, d.name+".qcow2"),
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
metaDataTempl = template.Must(template.New("meta-data.yaml").Parse(metaDataTemplate))
|
||||
userDataTempl = template.Must(template.New("user-data.yaml").Parse(userDataTemplate))
|
||||
)
|
||||
|
||||
// mkSeed makes the cloud-init seed ISO that is used to configure a VM with
|
||||
// tailscale.
|
||||
func mkSeed(t *testing.T, d Distro, sshKey, hostURL, tdir string, port int) {
|
||||
t.Helper()
|
||||
|
||||
dir := filepath.Join(tdir, d.name, "seed")
|
||||
os.MkdirAll(dir, 0700)
|
||||
|
||||
// make meta-data
|
||||
{
|
||||
fout, err := os.Create(filepath.Join(dir, "meta-data"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = metaDataTempl.Execute(fout, struct {
|
||||
ID string
|
||||
Hostname string
|
||||
}{
|
||||
ID: "31337",
|
||||
Hostname: d.name,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = fout.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// make user-data
|
||||
{
|
||||
fout, err := os.Create(filepath.Join(dir, "user-data"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = userDataTempl.Execute(fout, struct {
|
||||
SSHKey string
|
||||
HostURL string
|
||||
Hostname string
|
||||
Port int
|
||||
InstallPre string
|
||||
Password string
|
||||
}{
|
||||
SSHKey: strings.TrimSpace(sshKey),
|
||||
HostURL: hostURL,
|
||||
Hostname: d.name,
|
||||
Port: port,
|
||||
InstallPre: d.InstallPre(),
|
||||
Password: securePassword,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = fout.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-output", filepath.Join(dir, "seed.iso"),
|
||||
"-volid", "cidata", "-joliet", "-rock",
|
||||
filepath.Join(dir, "meta-data"),
|
||||
filepath.Join(dir, "user-data"),
|
||||
}
|
||||
|
||||
if hackOpenSUSE151UserData(t, d, dir) {
|
||||
args = append(args, filepath.Join(dir, "openstack"))
|
||||
}
|
||||
|
||||
run(t, tdir, "genisoimage", args...)
|
||||
}
|
||||
|
||||
// mkVM makes a KVM-accelerated virtual machine and prepares it for introduction
|
||||
// to the testcontrol server. The function it returns is for killing the virtual
|
||||
// machine when it is time for it to die.
|
||||
func mkVM(t *testing.T, n int, d Distro, sshKey, hostURL, tdir string) func() {
|
||||
t.Helper()
|
||||
|
||||
cdir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
t.Fatalf("can't find cache dir: %v", err)
|
||||
}
|
||||
cdir = filepath.Join(cdir, "tailscale", "vm-test")
|
||||
os.MkdirAll(filepath.Join(cdir, "qcow2"), 0755)
|
||||
|
||||
port := 23100 + n
|
||||
|
||||
fetchDistro(t, d)
|
||||
mkLayeredQcow(t, tdir, d)
|
||||
mkSeed(t, d, sshKey, hostURL, tdir, port)
|
||||
|
||||
driveArg := fmt.Sprintf("file=%s,if=virtio", filepath.Join(tdir, d.name+".qcow2"))
|
||||
|
||||
args := []string{
|
||||
"-machine", "pc-q35-5.1,accel=kvm,usb=off,vmport=off,dump-guest-core=off",
|
||||
"-netdev", fmt.Sprintf("user,hostfwd=::%d-:22,id=net0", port),
|
||||
"-device", "virtio-net-pci,netdev=net0,id=net0,mac=8a:28:5c:30:1f:25",
|
||||
"-m", fmt.Sprint(d.mem),
|
||||
"-boot", "c",
|
||||
"-drive", driveArg,
|
||||
"-cdrom", filepath.Join(tdir, d.name, "seed", "seed.iso"),
|
||||
"-vnc", fmt.Sprintf(":%d", n),
|
||||
"-smbios", "type=1,serial=ds=nocloud;h=" + d.name,
|
||||
}
|
||||
|
||||
t.Logf("running: qemu-system-x86_64 %s", strings.Join(args, " "))
|
||||
|
||||
cmd := exec.Command("qemu-system-x86_64", args...)
|
||||
cmd.Stdout = log.Writer()
|
||||
cmd.Stderr = log.Writer()
|
||||
err = cmd.Start()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if err := cmd.Process.Signal(syscall.Signal(0)); err != nil {
|
||||
t.Fatal("qemu is not running")
|
||||
}
|
||||
|
||||
return func() {
|
||||
err := cmd.Process.Kill()
|
||||
if err != nil {
|
||||
t.Errorf("can't kill %s (%d): %v", d.name, cmd.Process.Pid, err)
|
||||
}
|
||||
|
||||
cmd.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// ipMapping maps a hostname, SSH port and SSH IP together
|
||||
type ipMapping struct {
|
||||
name string
|
||||
port int
|
||||
ip string
|
||||
}
|
||||
|
||||
// TestVMIntegrationEndToEnd creates a virtual machine with qemu, installs
|
||||
// tailscale on it and then ensures that it connects to the network
|
||||
// successfully.
|
||||
func TestVMIntegrationEndToEnd(t *testing.T) {
|
||||
if !*runVMTests {
|
||||
t.Skip("not running integration tests (need --run-vm-tests)")
|
||||
}
|
||||
|
||||
os.Setenv("CGO_ENABLED", "0")
|
||||
|
||||
if _, err := exec.LookPath("qemu-system-x86_64"); err != nil {
|
||||
t.Logf("hint: nix-shell -p go -p qemu -p cdrkit --run 'go test --v --timeout=60m --run-vm-tests'")
|
||||
t.Fatalf("missing dependency: %v", err)
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("genisoimage"); err != nil {
|
||||
t.Logf("hint: nix-shell -p go -p qemu -p cdrkit --run 'go test --v --timeout=60m --run-vm-tests'")
|
||||
t.Fatalf("missing dependency: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
rex := distroRex.Unwrap()
|
||||
|
||||
ln, err := net.Listen("tcp", deriveBindhost(t)+":0")
|
||||
if err != nil {
|
||||
t.Fatalf("can't make TCP listener: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
t.Logf("host:port: %s", ln.Addr())
|
||||
|
||||
cs := &testcontrol.Server{}
|
||||
|
||||
var (
|
||||
ipMu sync.Mutex
|
||||
ipMap = map[string]ipMapping{}
|
||||
)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", cs)
|
||||
|
||||
// This handler will let the virtual machines tell the host information about that VM.
|
||||
// This is used to maintain a list of port->IP address mappings that are known to be
|
||||
// working. This allows later steps to connect over SSH. This returns no response to
|
||||
// clients because no response is needed.
|
||||
mux.HandleFunc("/myip/", func(w http.ResponseWriter, r *http.Request) {
|
||||
ipMu.Lock()
|
||||
defer ipMu.Unlock()
|
||||
|
||||
name := path.Base(r.URL.Path)
|
||||
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
port, err := strconv.Atoi(name)
|
||||
if err != nil {
|
||||
log.Panicf("bad port: %v", port)
|
||||
}
|
||||
distro := r.UserAgent()
|
||||
ipMap[distro] = ipMapping{distro, port, host}
|
||||
t.Logf("%s: %v", name, host)
|
||||
})
|
||||
|
||||
hs := &http.Server{Handler: mux}
|
||||
go hs.Serve(ln)
|
||||
|
||||
run(t, dir, "ssh-keygen", "-t", "ed25519", "-f", "machinekey", "-N", ``)
|
||||
pubkey, err := os.ReadFile(filepath.Join(dir, "machinekey.pub"))
|
||||
if err != nil {
|
||||
t.Fatalf("can't read ssh key: %v", err)
|
||||
}
|
||||
|
||||
privateKey, err := os.ReadFile(filepath.Join(dir, "machinekey"))
|
||||
if err != nil {
|
||||
t.Fatalf("can't read ssh private key: %v", err)
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("can't parse private key: %v", err)
|
||||
}
|
||||
|
||||
loginServer := fmt.Sprintf("http://%s", ln.Addr())
|
||||
t.Logf("loginServer: %s", loginServer)
|
||||
|
||||
tstest.FixLogs(t)
|
||||
defer tstest.UnfixLogs(t)
|
||||
|
||||
ramsem := semaphore.NewWeighted(int64(*vmRamLimit))
|
||||
bins := integration.BuildTestBinaries(t)
|
||||
|
||||
t.Run("do", func(t *testing.T) {
|
||||
for n, distro := range distros {
|
||||
n, distro := n, distro
|
||||
if rex.MatchString(distro.name) {
|
||||
t.Logf("%s matches %s", distro.name, rex)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run(distro.name, func(t *testing.T) {
|
||||
ctx, done := context.WithCancel(context.Background())
|
||||
defer done()
|
||||
|
||||
t.Parallel()
|
||||
|
||||
err := ramsem.Acquire(ctx, int64(distro.mem))
|
||||
if err != nil {
|
||||
t.Fatalf("can't acquire ram semaphore: %v", err)
|
||||
}
|
||||
defer ramsem.Release(int64(distro.mem))
|
||||
|
||||
cancel := mkVM(t, n, distro, string(pubkey), loginServer, dir)
|
||||
defer cancel()
|
||||
var ipm ipMapping
|
||||
|
||||
t.Run("wait-for-start", func(t *testing.T) {
|
||||
waiter := time.NewTicker(time.Second)
|
||||
defer waiter.Stop()
|
||||
var ok bool
|
||||
for {
|
||||
<-waiter.C
|
||||
ipMu.Lock()
|
||||
if ipm, ok = ipMap[distro.name]; ok {
|
||||
ipMu.Unlock()
|
||||
break
|
||||
}
|
||||
ipMu.Unlock()
|
||||
}
|
||||
})
|
||||
|
||||
testDistro(t, loginServer, signer, ipm, bins)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testDistro(t *testing.T, loginServer string, signer ssh.Signer, ipm ipMapping, bins *integration.Binaries) {
|
||||
t.Helper()
|
||||
port := ipm.port
|
||||
hostport := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
ccfg := &ssh.ClientConfig{
|
||||
User: "root",
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer), ssh.Password(securePassword)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
// NOTE(Xe): This deadline loop helps to make things a bit faster, centos
|
||||
// sometimes is slow at starting its sshd and will sometimes randomly kill
|
||||
// SSH sessions on transition to multi-user.target. I don't know why they
|
||||
// don't use socket activation.
|
||||
const maxRetries = 5
|
||||
var working bool
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
cli, err := ssh.Dial("tcp", hostport, ccfg)
|
||||
if err == nil {
|
||||
working = true
|
||||
cli.Close()
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
if !working {
|
||||
t.Fatalf("can't connect to %s, tried %d times", hostport, maxRetries)
|
||||
}
|
||||
|
||||
t.Logf("about to ssh into 127.0.0.1:%d", port)
|
||||
cli, err := ssh.Dial("tcp", hostport, ccfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copyBinaries(t, cli, bins)
|
||||
|
||||
timeout := 5 * time.Minute
|
||||
|
||||
e, _, err := expect.SpawnSSH(cli, timeout, expect.Verbose(true), expect.VerboseWriter(log.Writer()))
|
||||
if err != nil {
|
||||
t.Fatalf("%d: can't register a shell session: %v", port, err)
|
||||
}
|
||||
defer e.Close()
|
||||
|
||||
t.Log("opened session")
|
||||
|
||||
_, _, err = e.Expect(regexp.MustCompile(`(\#)`), timeout)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: can't get a shell: %v", port, err)
|
||||
}
|
||||
t.Logf("got shell for %d", port)
|
||||
err = e.Send("systemctl start tailscaled.service\n")
|
||||
if err != nil {
|
||||
t.Fatalf("can't send command to start tailscaled: %v", err)
|
||||
}
|
||||
_, _, err = e.Expect(regexp.MustCompile(`(\#)`), timeout)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: can't get a shell: %v", port, err)
|
||||
}
|
||||
err = e.Send(fmt.Sprintf("sudo tailscale up --login-server %s\n", loginServer))
|
||||
if err != nil {
|
||||
t.Fatalf("%d: can't send tailscale up command: %v", port, err)
|
||||
}
|
||||
_, _, err = e.Expect(regexp.MustCompile(`Success.`), timeout)
|
||||
if err != nil {
|
||||
t.Fatalf("not successful: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func copyBinaries(t *testing.T, conn *ssh.Client, bins *integration.Binaries) {
|
||||
cli, err := sftp.NewClient(conn)
|
||||
if err != nil {
|
||||
t.Fatalf("can't connect over sftp to copy binaries: %v", err)
|
||||
}
|
||||
|
||||
mkdir(t, cli, "/usr/bin")
|
||||
mkdir(t, cli, "/usr/sbin")
|
||||
mkdir(t, cli, "/etc/systemd/system")
|
||||
mkdir(t, cli, "/etc/default")
|
||||
|
||||
copyFile(t, cli, bins.Daemon, "/usr/sbin/tailscaled")
|
||||
copyFile(t, cli, bins.CLI, "/usr/bin/tailscale")
|
||||
|
||||
// TODO(Xe): revisit this life decision, hopefully before this assumption
|
||||
// breaks the test.
|
||||
copyFile(t, cli, "../../../cmd/tailscaled/tailscaled.defaults", "/etc/default/tailscaled")
|
||||
copyFile(t, cli, "../../../cmd/tailscaled/tailscaled.service", "/etc/systemd/system/tailscaled.service")
|
||||
|
||||
t.Log("tailscale installed!")
|
||||
}
|
||||
|
||||
func mkdir(t *testing.T, cli *sftp.Client, name string) {
|
||||
t.Helper()
|
||||
|
||||
err := cli.MkdirAll(name)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func copyFile(t *testing.T, cli *sftp.Client, localSrc, remoteDest string) {
|
||||
t.Helper()
|
||||
|
||||
fin, err := os.Open(localSrc)
|
||||
if err != nil {
|
||||
t.Fatalf("can't open: %v", err)
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
fi, err := fin.Stat()
|
||||
if err != nil {
|
||||
t.Fatalf("can't stat: %v", err)
|
||||
}
|
||||
|
||||
fout, err := cli.Create(remoteDest)
|
||||
if err != nil {
|
||||
t.Fatalf("can't create output file: %v", err)
|
||||
}
|
||||
|
||||
err = fout.Chmod(fi.Mode())
|
||||
if err != nil {
|
||||
fout.Close()
|
||||
t.Fatalf("can't chmod fout: %v", err)
|
||||
}
|
||||
|
||||
n, err := io.Copy(fout, fin)
|
||||
if err != nil {
|
||||
fout.Close()
|
||||
t.Fatalf("copy failed: %v", err)
|
||||
}
|
||||
|
||||
if fi.Size() != n {
|
||||
t.Fatalf("incorrect number of bytes copied: wanted: %d, got: %d", fi.Size(), n)
|
||||
}
|
||||
|
||||
err = fout.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("can't close fout on remote host: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func deriveBindhost(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
ifName, err := interfaces.DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var ret string
|
||||
err = interfaces.ForeachInterfaceAddress(func(i interfaces.Interface, prefix netaddr.IPPrefix) {
|
||||
if ret != "" || i.Name != ifName {
|
||||
return
|
||||
}
|
||||
ret = prefix.IP().String()
|
||||
})
|
||||
if ret != "" {
|
||||
return ret
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Fatal("can't find a bindhost")
|
||||
return "unreachable"
|
||||
}
|
||||
|
||||
func TestDeriveBindhost(t *testing.T) {
|
||||
t.Log(deriveBindhost(t))
|
||||
}
|
||||
|
||||
const metaDataTemplate = `instance-id: {{.ID}}
|
||||
local-hostname: {{.Hostname}}`
|
||||
|
||||
const userDataTemplate = `#cloud-config
|
||||
#vim:syntax=yaml
|
||||
|
||||
cloud_config_modules:
|
||||
- runcmd
|
||||
|
||||
cloud_final_modules:
|
||||
- [users-groups, always]
|
||||
- [scripts-user, once-per-instance]
|
||||
|
||||
users:
|
||||
- name: root
|
||||
ssh-authorized-keys:
|
||||
- {{.SSHKey}}
|
||||
- name: ts
|
||||
plain_text_passwd: {{.Password}}
|
||||
groups: [ wheel ]
|
||||
sudo: [ "ALL=(ALL) NOPASSWD:ALL" ]
|
||||
shell: /bin/sh
|
||||
ssh-authorized-keys:
|
||||
- {{.SSHKey}}
|
||||
|
||||
write_files:
|
||||
- path: /etc/cloud/cloud.cfg.d/80_disable_network_after_firstboot.cfg
|
||||
content: |
|
||||
# Disable network configuration after first boot
|
||||
network:
|
||||
config: disabled
|
||||
|
||||
runcmd:
|
||||
{{.InstallPre}}
|
||||
- [ curl, "{{.HostURL}}/myip/{{.Port}}", "-H", "User-Agent: {{.Hostname}}" ]
|
||||
`
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package strbuilder defines a string builder type that allocates
|
||||
// less than the standard library's strings.Builder by using a
|
||||
// sync.Pool, so it doesn't matter if the compiler can't prove that
|
||||
// the builder doesn't escape into the fmt package, etc.
|
||||
package strbuilder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var pool = sync.Pool{
|
||||
New: func() interface{} { return new(Builder) },
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
bb bytes.Buffer
|
||||
scratch [20]byte // long enough for MinInt64, MaxUint64
|
||||
locked bool // in pool, not for use
|
||||
}
|
||||
|
||||
// Get returns a new or reused string Builder.
|
||||
func Get() *Builder {
|
||||
b := pool.Get().(*Builder)
|
||||
b.bb.Reset()
|
||||
b.locked = false
|
||||
return b
|
||||
}
|
||||
|
||||
// String both returns the Builder's string, and returns the builder
|
||||
// to the pool.
|
||||
func (b *Builder) String() string {
|
||||
if b.locked {
|
||||
panic("String called twiced on Builder")
|
||||
}
|
||||
s := b.bb.String()
|
||||
b.locked = true
|
||||
pool.Put(b)
|
||||
return s
|
||||
}
|
||||
|
||||
func (b *Builder) WriteByte(v byte) error {
|
||||
return b.bb.WriteByte(v)
|
||||
}
|
||||
|
||||
func (b *Builder) WriteString(s string) (int, error) {
|
||||
return b.bb.WriteString(s)
|
||||
}
|
||||
|
||||
func (b *Builder) Write(p []byte) (int, error) {
|
||||
return b.bb.Write(p)
|
||||
}
|
||||
|
||||
func (b *Builder) WriteInt(v int64) {
|
||||
b.Write(strconv.AppendInt(b.scratch[:0], v, 10))
|
||||
}
|
||||
|
||||
func (b *Builder) WriteUint(v uint64) {
|
||||
b.Write(strconv.AppendUint(b.scratch[:0], v, 10))
|
||||
}
|
||||
|
||||
// Grow grows the buffer's capacity, if necessary, to guarantee space
|
||||
// for another n bytes. After Grow(n), at least n bytes can be written
|
||||
// to the buffer without another allocation. If n is negative, Grow
|
||||
// will panic. If the buffer can't grow it will panic with
|
||||
// ErrTooLarge.
|
||||
func (b *Builder) Grow(n int) {
|
||||
b.bb.Grow(n)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package strbuilder
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuilder(t *testing.T) {
|
||||
const want = "Hello, world 123 -456!"
|
||||
bang := []byte("!")
|
||||
var got string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
sb := Get()
|
||||
sb.WriteString("Hello, world ")
|
||||
sb.WriteUint(123)
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteInt(-456)
|
||||
sb.Write(bang)
|
||||
got = sb.String()
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
if allocs != 1 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies scratch buf is large enough.
|
||||
func TestIntBounds(t *testing.T) {
|
||||
const want = "-9223372036854775808 9223372036854775807 18446744073709551615"
|
||||
var got string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
sb := Get()
|
||||
sb.WriteInt(math.MinInt64)
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteInt(math.MaxInt64)
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteUint(math.MaxUint64)
|
||||
got = sb.String()
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
if allocs != 1 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
package wgkey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
@@ -72,10 +71,11 @@ func ParsePrivateHex(v string) (Private, error) {
|
||||
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) 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) AppendTo(b []byte) []byte { return appendKey(b, "", k) }
|
||||
|
||||
func (k *Key) ShortString() string {
|
||||
// The goal here is to generate "[" + base64.StdEncoding.EncodeToString(k[:])[:5] + "]".
|
||||
@@ -178,13 +178,17 @@ func (k *Private) Public() Key {
|
||||
return (Key)(p)
|
||||
}
|
||||
|
||||
func (k Private) MarshalText() ([]byte, error) {
|
||||
// TODO(josharian): use encoding/hex instead?
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `privkey:%x`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
func appendKey(base []byte, prefix string, k [32]byte) []byte {
|
||||
ret := append(base, make([]byte, len(prefix)+64)...)
|
||||
buf := ret[len(base):]
|
||||
copy(buf, prefix)
|
||||
hex.Encode(buf[len(prefix):], k[:])
|
||||
return ret
|
||||
}
|
||||
|
||||
func (k Private) MarshalText() ([]byte, error) { return appendKey(nil, "privkey:", k), nil }
|
||||
func (k Private) AppendTo(b []byte) []byte { return appendKey(b, "privkey:", k) }
|
||||
|
||||
func (k *Private) UnmarshalText(b []byte) error {
|
||||
s := string(b)
|
||||
if !strings.HasPrefix(s, `privkey:`) {
|
||||
|
||||
@@ -21,46 +21,48 @@ const (
|
||||
type FQDN string
|
||||
|
||||
func ToFQDN(s string) (FQDN, error) {
|
||||
if isValidFQDN(s) {
|
||||
return FQDN(s), nil
|
||||
}
|
||||
if len(s) == 0 || s == "." {
|
||||
return FQDN("."), nil
|
||||
}
|
||||
|
||||
if s[len(s)-1] == '.' {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
if s[0] == '.' {
|
||||
s = s[1:]
|
||||
}
|
||||
if len(s) > maxNameLength {
|
||||
raw := s
|
||||
totalLen := len(s)
|
||||
if s[len(s)-1] == '.' {
|
||||
s = s[:len(s)-1]
|
||||
} else {
|
||||
totalLen += 1 // account for missing dot
|
||||
}
|
||||
if totalLen > maxNameLength {
|
||||
return "", fmt.Errorf("%q is too long to be a DNS name", s)
|
||||
}
|
||||
|
||||
fs := strings.Split(s, ".")
|
||||
for _, f := range fs {
|
||||
if !validLabel(f) {
|
||||
return "", fmt.Errorf("%q is not a valid DNS label", f)
|
||||
st := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] != '.' {
|
||||
continue
|
||||
}
|
||||
label := s[st:i]
|
||||
// You might be tempted to do further validation of the
|
||||
// contents of labels here, based on the hostname rules in RFC
|
||||
// 1123. However, DNS labels are not always subject to
|
||||
// hostname rules. In general, they can contain any non-zero
|
||||
// byte sequence, even though in practice a more restricted
|
||||
// set is used.
|
||||
//
|
||||
// See https://github.com/tailscale/tailscale/issues/2024 for more.
|
||||
if len(label) == 0 || len(label) > maxLabelLength {
|
||||
return "", fmt.Errorf("%q is not a valid DNS label", label)
|
||||
}
|
||||
st = i + 1
|
||||
}
|
||||
|
||||
return FQDN(s + "."), nil
|
||||
}
|
||||
|
||||
func validLabel(s string) bool {
|
||||
if len(s) == 0 || len(s) > maxLabelLength {
|
||||
return false
|
||||
if raw[len(raw)-1] != '.' {
|
||||
raw = raw + "."
|
||||
}
|
||||
if !isalphanum(s[0]) || !isalphanum(s[len(s)-1]) {
|
||||
return false
|
||||
}
|
||||
for i := 1; i < len(s)-1; i++ {
|
||||
if !isalphanum(s[i]) && s[i] != '-' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return FQDN(raw), nil
|
||||
}
|
||||
|
||||
// WithTrailingDot returns f as a string, with a trailing dot.
|
||||
@@ -92,51 +94,6 @@ func (f FQDN) Contains(other FQDN) bool {
|
||||
return strings.HasSuffix(other.WithTrailingDot(), cmp)
|
||||
}
|
||||
|
||||
// isValidFQDN reports whether s is already a valid FQDN, without
|
||||
// allocating.
|
||||
func isValidFQDN(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
if len(s) > maxNameLength {
|
||||
return false
|
||||
}
|
||||
// DNS root name.
|
||||
if s == "." {
|
||||
return true
|
||||
}
|
||||
// Missing trailing dot.
|
||||
if s[len(s)-1] != '.' {
|
||||
return false
|
||||
}
|
||||
// Leading dots not allowed.
|
||||
if s[0] == '.' {
|
||||
return false
|
||||
}
|
||||
|
||||
st := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] != '.' {
|
||||
continue
|
||||
}
|
||||
label := s[st:i]
|
||||
if len(label) == 0 || len(label) > maxLabelLength {
|
||||
return false
|
||||
}
|
||||
if !isalphanum(label[0]) || !isalphanum(label[len(label)-1]) {
|
||||
return false
|
||||
}
|
||||
for j := 1; j < len(label)-1; j++ {
|
||||
if !isalphanum(label[j]) && label[j] != '-' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
st = i + 1
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SanitizeLabel takes a string intended to be a DNS name label
|
||||
// and turns it into a valid name label according to RFC 1035.
|
||||
func SanitizeLabel(label string) string {
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestFQDN(t *testing.T) {
|
||||
{".foo.com", "foo.com.", false, 2},
|
||||
{"com", "com.", false, 1},
|
||||
{"www.tailscale.com", "www.tailscale.com.", false, 3},
|
||||
{"_ssh._tcp.tailscale.com", "_ssh._tcp.tailscale.com.", false, 4},
|
||||
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com", "", true, 0},
|
||||
{strings.Repeat("aaaaa.", 60) + "com", "", true, 0},
|
||||
{"foo..com", "", true, 0},
|
||||
@@ -184,3 +185,24 @@ func TestTrimSuffix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sinkFQDN FQDN
|
||||
|
||||
func BenchmarkToFQDN(b *testing.B) {
|
||||
tests := []string{
|
||||
"www.tailscale.com.",
|
||||
"www.tailscale.com",
|
||||
".www.tailscale.com",
|
||||
"_ssh._tcp.www.tailscale.com.",
|
||||
"_ssh._tcp.www.tailscale.com",
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
b.Run(test, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkFQDN, _ = ToFQDN(test)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Placeholder that indicates this directory is a valid go package,
|
||||
// but that redo must 'redo all' in this directory before it can
|
||||
// be imported.
|
||||
|
||||
package version
|
||||
@@ -1,2 +0,0 @@
|
||||
redo-ifchange ver.go version.xcconfig version.h
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
rm -f *~ .*~ describe.txt long.txt short.txt version.xcconfig ver.go version.h version version-info.sh
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func mkversion(t *testing.T, gitHash, otherHash string, major, minor, patch, changeCount int) (string, bool) {
|
||||
t.Helper()
|
||||
bs, err := exec.Command("./version.sh", gitHash, otherHash, strconv.Itoa(major), strconv.Itoa(minor), strconv.Itoa(patch), strconv.Itoa(changeCount)).CombinedOutput()
|
||||
out := strings.TrimSpace(string(bs))
|
||||
if err != nil {
|
||||
return out, false
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func TestMkversion(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skip test on Windows, because there is no shell to execute mkversion.sh.")
|
||||
}
|
||||
tests := []struct {
|
||||
gitHash, otherHash string
|
||||
major, minor, patch, changeCount int
|
||||
want string
|
||||
}{
|
||||
{"abcdef", "", 0, 98, 0, 0, `
|
||||
VERSION_SHORT="0.98.0"
|
||||
VERSION_LONG="0.98.0-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="100.98.0"
|
||||
VERSION_WINRES="0,98,0,0"`},
|
||||
{"abcdef", "", 0, 98, 1, 0, `
|
||||
VERSION_SHORT="0.98.1"
|
||||
VERSION_LONG="0.98.1-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="100.98.1"
|
||||
VERSION_WINRES="0,98,1,0"`},
|
||||
{"abcdef", "", 1, 1, 0, 37, `
|
||||
VERSION_SHORT="1.1.1037"
|
||||
VERSION_LONG="1.1.1037-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="101.1.1037"
|
||||
VERSION_WINRES="1,1,1037,0"`},
|
||||
{"abcdef", "", 1, 2, 9, 0, `
|
||||
VERSION_SHORT="1.2.9"
|
||||
VERSION_LONG="1.2.9-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="101.2.9"
|
||||
VERSION_WINRES="1,2,9,0"`},
|
||||
{"abcdef", "", 1, 15, 0, 129, `
|
||||
VERSION_SHORT="1.15.129"
|
||||
VERSION_LONG="1.15.129-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="101.15.129"
|
||||
VERSION_WINRES="1,15,129,0"`},
|
||||
{"abcdef", "", 1, 2, 0, 17, `
|
||||
VERSION_SHORT="1.2.0"
|
||||
VERSION_LONG="1.2.0-17-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="101.2.0"
|
||||
VERSION_WINRES="1,2,0,0"`},
|
||||
{"abcdef", "defghi", 1, 15, 0, 129, `
|
||||
VERSION_SHORT="1.15.129"
|
||||
VERSION_LONG="1.15.129-tabcdef-gdefghi"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH="defghi"
|
||||
VERSION_XCODE="101.15.129"
|
||||
VERSION_WINRES="1,15,129,0"`},
|
||||
{"abcdef", "", 0, 99, 5, 0, ""}, // unstable, patch number not allowed
|
||||
{"abcdef", "", 0, 99, 5, 123, ""}, // unstable, patch number not allowed
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
want := strings.ReplaceAll(strings.TrimSpace(test.want), " ", "")
|
||||
got, ok := mkversion(t, test.gitHash, test.otherHash, test.major, test.minor, test.patch, test.changeCount)
|
||||
invoc := fmt.Sprintf("version.sh %s %s %d %d %d %d", test.gitHash, test.otherHash, test.major, test.minor, test.patch, test.changeCount)
|
||||
if want == "" && ok {
|
||||
t.Errorf("%s ok=true, want false", invoc)
|
||||
continue
|
||||
}
|
||||
if diff := cmp.Diff(got, want); want != "" && diff != "" {
|
||||
t.Errorf("%s wrong output (-got+want):\n%s", invoc, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
redo-ifchange version-info.sh ver.go.in
|
||||
|
||||
. ./version-info.sh
|
||||
|
||||
sed -e "s/{LONGVER}/$VERSION_LONG/g" \
|
||||
-e "s/{SHORTVER}/$VERSION_SHORT/g" \
|
||||
-e "s/{GITCOMMIT}/$VERSION_GIT_HASH/g" \
|
||||
-e "s/{EXTRAGITCOMMIT}/$VERSION_EXTRA_HASH/g" \
|
||||
<ver.go.in >$3
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build redo
|
||||
|
||||
package version
|
||||
|
||||
const Long = "{LONGVER}"
|
||||
const Short = "{SHORTVER}"
|
||||
const LONG = Long
|
||||
const SHORT = Short
|
||||
const GitCommit = "{GITCOMMIT}"
|
||||
const ExtraGitCommit = "{EXTRAGITCOMMIT}"
|
||||
@@ -1,3 +0,0 @@
|
||||
./version.sh ../.. >$3
|
||||
redo-always
|
||||
redo-stamp <$3
|
||||
@@ -2,31 +2,30 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !redo,!xversion
|
||||
|
||||
// Package version provides the version that the binary was built at.
|
||||
package version
|
||||
|
||||
// Long is a full version number for this build, of the form
|
||||
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
|
||||
// provided.
|
||||
const Long = "date.20210505"
|
||||
var Long = "date.20210603"
|
||||
|
||||
// Short is a short version number for this build, of the form
|
||||
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
|
||||
const Short = Long
|
||||
var Short = ""
|
||||
|
||||
// LONG is a deprecated alias for Long. Don't use it.
|
||||
const LONG = Long
|
||||
|
||||
// SHORT is a deprecated alias for Short. Don't use it.
|
||||
const SHORT = Short
|
||||
func init() {
|
||||
if Short == "" {
|
||||
// If it hasn't been link-stamped with -X:
|
||||
Short = Long
|
||||
}
|
||||
}
|
||||
|
||||
// GitCommit, if non-empty, is the git commit of the
|
||||
// github.com/tailscale/tailscale repository at which Tailscale was
|
||||
// built. Its format is the one returned by `git describe --always
|
||||
// --exclude "*" --dirty --abbrev=200`.
|
||||
const GitCommit = ""
|
||||
var GitCommit = ""
|
||||
|
||||
// ExtraGitCommit, if non-empty, is the git commit of a "supplemental"
|
||||
// repository at which Tailscale was built. Its format is the same as
|
||||
@@ -38,4 +37,4 @@ const GitCommit = ""
|
||||
// Android OSS repository). Together, GitCommit and ExtraGitCommit
|
||||
// exactly describe what repositories and commits were used in a
|
||||
// build.
|
||||
const ExtraGitCommit = ""
|
||||
var ExtraGitCommit = ""
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
redo-ifchange version-info.sh
|
||||
|
||||
. ./version-info.sh
|
||||
|
||||
cat >$3 <<EOF
|
||||
#define TAILSCALE_VERSION_LONG "$VERSION_LONG"
|
||||
#define TAILSCALE_VERSION_SHORT "$VERSION_SHORT"
|
||||
#define TAILSCALE_VERSION_WIN_RES $VERSION_WINRES
|
||||
EOF
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user