Compare commits

..

1 Commits

Author SHA1 Message Date
Christine Dodrill
ed69ae4684 scripts/installer: support all distros and oses
As well function entirely headlessly. This was used to test the work
done in #1922. The only thing it fails on is Debian bullseye, which will
need more work due to it being prerelase. More work is required there
but that is out of scope for now.

Signed-off-by: Christine Dodrill <me@christine.website>
2021-05-26 13:00:01 -04:00
138 changed files with 1690 additions and 4378 deletions

View File

@@ -1,36 +0,0 @@
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"

View File

@@ -11,36 +11,6 @@
set -eu
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)
eval $(./version/version.sh)
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}" "$@"
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}" "$@"

View File

@@ -256,25 +256,3 @@ 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
}

View File

@@ -112,13 +112,13 @@ func tailscaleIP(who *apitype.WhoIsResponse) string {
return ""
}
for _, nodeIP := range who.Node.Addresses {
if nodeIP.IP().Is4() && nodeIP.IsSingleIP() {
return nodeIP.IP().String()
if nodeIP.IP.Is4() && nodeIP.IsSingleIP() {
return nodeIP.IP.String()
}
}
for _, nodeIP := range who.Node.Addresses {
if nodeIP.IsSingleIP() {
return nodeIP.IP().String()
return nodeIP.IP.String()
}
}
return ""

View File

@@ -21,9 +21,6 @@ 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 {

View File

@@ -194,7 +194,7 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, lastSe
for _, ft := range fts {
n := ft.Node
for _, a := range n.Addresses {
if a.IP() != ip {
if a.IP != ip {
continue
}
if n.LastSeen != nil {
@@ -301,7 +301,7 @@ func runCpTargets(ctx context.Context, args []string) error {
if detail != "" {
detail = "\t" + detail
}
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP, n.ComputedName, detail)
}
return nil
}

View File

@@ -164,10 +164,10 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
routes = append(routes, r)
}
sort.Slice(routes, func(i, j int) bool {
if routes[i].Bits() != routes[j].Bits() {
return routes[i].Bits() < routes[j].Bits()
if routes[i].Bits != routes[j].Bits {
return routes[i].Bits < routes[j].Bits
}
return routes[i].IP().Less(routes[j].IP())
return routes[i].IP.Less(routes[j].IP)
})
var exitNodeIP netaddr.IP
@@ -230,9 +230,7 @@ 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
if defaultNetfilterMode() != "off" {
warnf("netfilter=off; configure iptables yourself.")
}
warnf("netfilter=off; configure iptables yourself.")
default:
return nil, fmt.Errorf("invalid value --netfilter-mode=%q", upArgs.netfilterMode)
}
@@ -268,7 +266,7 @@ func runUp(ctx context.Context, args []string) error {
}
if distro.Get() == distro.Synology {
notSupported := "not supported on Synology; see https://github.com/tailscale/tailscale/issues/1995"
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
if upArgs.acceptRoutes {
return errors.New("--accept-routes is " + notSupported)
}
@@ -725,10 +723,10 @@ func fmtFlagValueArg(flagName string, val interface{}) string {
func hasExitNodeRoutes(rr []netaddr.IPPrefix) bool {
var v4, v6 bool
for _, r := range rr {
if r.Bits() == 0 {
if r.IP().Is4() {
if r.Bits == 0 {
if r.IP.Is4() {
v4 = true
} else if r.IP().Is6() {
} else if r.IP.Is6() {
v6 = true
}
}
@@ -745,7 +743,7 @@ func withoutExitNodes(rr []netaddr.IPPrefix) []netaddr.IPPrefix {
}
var out []netaddr.IPPrefix
for _, r := range rr {
if r.Bits() > 0 {
if r.Bits > 0 {
out = append(out, r)
}
}

View File

@@ -5,7 +5,6 @@
package cli
import (
"bufio"
"bytes"
"context"
_ "embed"
@@ -16,13 +15,11 @@ 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"
@@ -85,63 +82,17 @@ func runWeb(ctx context.Context, args []string) error {
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
}
// authorize checks whether the provided user has access to the web UI.
func authorize(name string) error {
func auth() (string, error) {
if distro.Get() == distro.Synology {
return authorizeSynology(name)
}
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
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)
}
agLine = s.Text()
break
return string(out), nil
}
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
return "", nil
}
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
@@ -247,13 +198,8 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
return
}
user, err := authenticate()
user, err := auth()
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
}
@@ -268,8 +214,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
url, err := tailscaleUpForceReauth(r.Context())
if err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
json.NewEncoder(w).Encode(mi{"error": err})
return
}
json.NewEncoder(w).Encode(mi{"url": url})
@@ -375,10 +320,6 @@ 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")
}

View File

@@ -1,7 +1,6 @@
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/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi 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
@@ -49,13 +48,14 @@ 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

View File

@@ -1,43 +1,41 @@
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/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi 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 tailscale.com/wgengine/monitor+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/mdlayher/netlink+
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
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
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+
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+
@@ -71,7 +69,6 @@ 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+
@@ -118,6 +115,7 @@ 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+
@@ -130,6 +128,7 @@ 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
@@ -144,7 +143,6 @@ 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+
@@ -156,7 +154,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 golang.zx2c4.com/wireguard/device+
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
@@ -165,7 +163,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 golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
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+
@@ -173,15 +171,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 golang.zx2c4.com/wireguard/device
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/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/mdlayher/netlink+
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
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+
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

View File

@@ -266,7 +266,7 @@ func run() error {
if err != nil {
return nil, err
}
if ns != nil && useNetstackForIP(ipp.IP()) {
if ns != nil && useNetstackForIP(ipp.IP) {
return ns.DialContextTCP(ctx, addr)
}
var d net.Dialer

View File

@@ -21,6 +21,7 @@ import (
"context"
"fmt"
"log"
"net"
"os"
"time"
@@ -31,9 +32,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"
@@ -143,13 +144,13 @@ func beFirewallKillswitch() bool {
luid, err := winipcfg.LUIDFromGUID(&guid)
if err != nil {
log.Fatalf("no interface with GUID %q: %v", guid, err)
log.Fatalf("no interface with GUID %q", guid)
}
noProtection := false
var dnsIPs []net.IP // unused in called code.
start := time.Now()
if _, err := wf.New(uint64(luid)); err != nil {
log.Fatalf("filewall creation failed: %v", err)
}
firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs)
log.Printf("killswitch enabled, took %s", time.Since(start))
// Block until the monitor goroutine shuts us down.

View File

@@ -576,12 +576,9 @@ 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 loginFin, logoutFin *empty.Message
var fin *empty.Message
if state == StateAuthenticated {
loginFin = new(empty.Message)
}
if state == StateNotAuthenticated {
logoutFin = new(empty.Message)
fin = new(empty.Message)
}
if nm != nil && loggedIn && synced {
pp := c.direct.GetPersist()
@@ -592,13 +589,12 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
nm = nil
}
new := Status{
LoginFinished: loginFin,
LogoutFinished: logoutFin,
URL: url,
Persist: p,
NetMap: nm,
Hostinfo: hi,
State: state,
LoginFinished: fin,
URL: url,
Persist: p,
NetMap: nm,
Hostinfo: hi,
State: state,
}
if err != nil {
new.Err = err.Error()
@@ -716,9 +712,3 @@ 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)
}

View File

@@ -74,7 +74,4 @@ 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
}

View File

@@ -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", "LogoutFinished", "Err", "URL", "NetMap", "State", "Persist", "Hostinfo"}
equalHandles := []string{"LoginFinished", "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)

View File

@@ -32,7 +32,6 @@ 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"
@@ -67,7 +66,6 @@ type Direct struct {
debugFlags []string
keepSharerAndUserSplit bool
skipIPForwardingCheck bool
pinger Pinger
mu sync.Mutex // mutex guards the following fields
serverKey wgkey.Key
@@ -105,18 +103,6 @@ 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 {
@@ -179,7 +165,6 @@ 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())
@@ -1106,7 +1091,7 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
localIPs := map[netaddr.IP]bool{}
for _, addrs := range state.InterfaceIPs {
for _, pfx := range addrs {
localIPs[pfx.IP()] = true
localIPs[pfx.IP] = true
}
}
@@ -1115,10 +1100,10 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
// It's possible to advertise a route to one of the local
// machine's local IPs. IP forwarding isn't required for this
// to work, so we shouldn't warn for such exports.
if r.IsSingleIP() && localIPs[r.IP()] {
if r.IsSingleIP() && localIPs[r.IP] {
continue
}
if r.IP().Is4() {
if r.IP.Is4() {
v4Routes = true
} else {
v6Routes = true
@@ -1226,50 +1211,3 @@ 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
}

View File

@@ -86,7 +86,7 @@ func TestNewDirect(t *testing.T) {
func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
for _, port := range ports {
ret = append(ret, tailcfg.Endpoint{
Addr: netaddr.IPPortFrom(netaddr.IP{}, port),
Addr: netaddr.IPPort{Port: port},
})
}
return

View File

@@ -18,7 +18,7 @@ import (
"fmt"
"sync"
"github.com/tailscale/certstore"
"github.com/github/certstore"
"tailscale.com/tailcfg"
"tailscale.com/types/wgkey"
"tailscale.com/util/winutil"

View File

@@ -64,12 +64,11 @@ func (s State) String() string {
}
type Status struct {
_ 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
_ 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
// The internal state should not be exposed outside this
// package, but we have some automated tests elsewhere that need to
@@ -87,7 +86,6 @@ 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) &&

View File

@@ -147,9 +147,9 @@ const epLength = 16 + 2 // 16 byte IP address + 2 byte port
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
for _, ipp := range m.MyNumber {
a := ipp.IP().As16()
a := ipp.IP.As16()
copy(p[:], a[:])
binary.BigEndian.PutUint16(p[16:], ipp.Port())
binary.BigEndian.PutUint16(p[16:], ipp.Port)
p = p[epLength:]
}
return ret
@@ -164,9 +164,10 @@ func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
for len(p) > 0 {
var a [16]byte
copy(a[:], p)
m.MyNumber = append(m.MyNumber, netaddr.IPPortFrom(
netaddr.IPFrom16(a),
binary.BigEndian.Uint16(p[16:18])))
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
IP: netaddr.IPFrom16(a),
Port: binary.BigEndian.Uint16(p[16:18]),
})
p = p[epLength:]
}
return m, nil
@@ -186,9 +187,9 @@ const pongLen = 12 + 16 + 2
func (m *Pong) AppendMarshal(b []byte) []byte {
ret, d := appendMsgHeader(b, TypePong, v0, pongLen)
d = d[copy(d, m.TxID[:]):]
ip16 := m.Src.IP().As16()
ip16 := m.Src.IP.As16()
d = d[copy(d, ip16[:]):]
binary.BigEndian.PutUint16(d, m.Src.Port())
binary.BigEndian.PutUint16(d, m.Src.Port)
return ret
}
@@ -200,10 +201,10 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
copy(m.TxID[:], p)
p = p[12:]
srcIP, _ := netaddr.FromStdIP(net.IP(p[:16]))
m.Src.IP, _ = netaddr.FromStdIP(net.IP(p[:16]))
p = p[16:]
port := binary.BigEndian.Uint16(p)
m.Src = netaddr.IPPortFrom(srcIP, port)
m.Src.Port = binary.BigEndian.Uint16(p)
return m, nil
}

63
go.mod
View File

@@ -3,47 +3,48 @@ module tailscale.com
go 1.16
require (
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/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/go-multierror/multierror v1.0.2
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/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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.12.2
github.com/klauspost/compress v1.10.10
github.com/kr/pty v1.1.8
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/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/peterbourgon/ff/v2 v2.0.0
github.com/pkg/sftp v1.13.0
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
github.com/pkg/errors v0.9.1 // indirect
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-20210513164829-c07d793c2f9a
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
golang.org/x/net v0.0.0-20210510120150-4163338589ed
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
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
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-20210511181906-37180328850c
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
inet.af/wf v0.0.0-20210516214145-a5343001b756
inet.af/peercred v0.0.0-20210302202138-56e694897155
inet.af/wf v0.0.0-20210424212123-eaa011a774a4
rsc.io/goversion v1.2.0
)
replace github.com/github/certstore => github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2

880
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -9,28 +9,26 @@ package deephash
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"reflect"
"strconv"
"sync"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/wgkey"
)
func calcHash(v interface{}) string {
func Hash(v ...interface{}) string {
h := sha256.New()
b := bufio.NewWriterSize(h, h.BlockSize())
scratch := make([]byte, 0, 128)
printTo(b, v, scratch)
// 64 matches the chunk size in crypto/sha256/sha256.go
b := bufio.NewWriterSize(h, 64)
Print(b, v)
b.Flush()
scratch = h.Sum(scratch[:0])
hex.Encode(scratch[:cap(scratch)], scratch[:sha256.Size])
return string(scratch[:sha256.Size*2])
return fmt.Sprintf("%x", h.Sum(nil))
}
// UpdateHash sets last to the hash of v and reports whether its value changed.
func UpdateHash(last *string, v ...interface{}) (changed bool) {
sig := calcHash(v)
sig := Hash(v)
if *last != sig {
*last = sig
return true
@@ -38,30 +36,81 @@ func UpdateHash(last *string, v ...interface{}) (changed bool) {
return false
}
func printTo(w *bufio.Writer, v interface{}, scratch []byte) {
print(w, reflect.ValueOf(v), make(map[uintptr]bool), scratch)
func Print(w *bufio.Writer, v ...interface{}) {
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
}
var appenderToType = reflect.TypeOf((*appenderTo)(nil)).Elem()
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{})
)
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) {
func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool) {
if !v.IsValid() {
return true
return
}
// Special case some common types.
if v.CanInterface() {
// 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
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
}
}
@@ -72,45 +121,43 @@ func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool, scratch [
case reflect.Ptr:
ptr := v.Pointer()
if visited[ptr] {
return false
return
}
visited[ptr] = true
return print(w, v.Elem(), visited, scratch)
print(w, v.Elem(), visited)
return
case reflect.Struct:
acyclic = true
w.WriteString("struct{\n")
for i, n := 0, v.NumField(); i < n; i++ {
fmt.Fprintf(w, " [%d]: ", i)
if !print(w, v.Field(i), visited, scratch) {
acyclic = false
}
print(w, v.Field(i), visited)
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 true
return
}
fmt.Fprintf(w, "[%d]{\n", v.Len())
acyclic = true
for i, ln := 0, v.Len(); i < ln; i++ {
fmt.Fprintf(w, " [%d]: ", i)
if !print(w, v.Index(i), visited, scratch) {
acyclic = false
}
print(w, v.Index(i), visited)
w.WriteString("\n")
}
w.WriteString("}\n")
return acyclic
case reflect.Interface:
return print(w, v.Elem(), visited, scratch)
print(w, v.Elem(), visited)
case reflect.Map:
if hashMapAcyclic(w, v, visited, scratch) {
return true
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")
}
return hashMapFallback(w, v, visited, scratch)
w.WriteString("}\n")
case reflect.String:
w.WriteString(v.String())
case reflect.Bool:
@@ -118,109 +165,10 @@ func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool, scratch [
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:
scratch = strconv.AppendUint(scratch[:0], v.Uint(), 10)
w.Write(scratch)
fmt.Fprintf(w, "%v", v.Uint())
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
}

View File

@@ -5,10 +5,6 @@
package deephash
import (
"bufio"
"bytes"
"fmt"
"reflect"
"testing"
"inet.af/netaddr"
@@ -18,15 +14,15 @@ import (
"tailscale.com/wgengine/wgcfg"
)
func TestDeepHash(t *testing.T) {
func TestDeepPrint(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 := calcHash(v)
hash1 := Hash(v)
t.Logf("hash: %v", hash1)
for i := 0; i < 20; i++ {
hash2 := calcHash(getVal())
hash2 := Hash(getVal())
if hash1 != hash2 {
t.Error("second hash didn't match")
}
@@ -37,7 +33,7 @@ func getVal() []interface{} {
return []interface{}{
&wgcfg.Config{
Name: "foo",
Addresses: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPFrom16([16]byte{3: 3}), 5)},
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
Peers: []wgcfg.Peer{
{
Endpoints: wgcfg.Endpoints{
@@ -55,23 +51,14 @@ 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,
},
}
}
@@ -80,57 +67,6 @@ func BenchmarkHash(b *testing.B) {
b.ReportAllocs()
v := getVal()
for i := 0; i < b.N; i++ {
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")
}
Hash(v)
}
}

View File

@@ -1,37 +0,0 @@
// 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()
}

View File

@@ -1,42 +0,0 @@
// 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
}

View File

@@ -44,13 +44,11 @@ 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"
@@ -358,14 +356,14 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
var tailAddr4 string
var tailscaleIPs = make([]netaddr.IP, 0, len(p.Addresses))
for _, addr := range p.Addresses {
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP()) {
if addr.IP().Is4() && tailAddr4 == "" {
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) {
if addr.IP.Is4() && tailAddr4 == "" {
// The peer struct previously only allowed a single
// Tailscale IP address. For compatibility for a few releases starting
// with 1.8, keep it pulled out as IPv4-only for a bit.
tailAddr4 = addr.IP().String()
tailAddr4 = addr.IP.String()
}
tailscaleIPs = append(tailscaleIPs, addr.IP())
tailscaleIPs = append(tailscaleIPs, addr.IP)
}
}
sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
@@ -392,10 +390,10 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
func (b *LocalBackend) WhoIs(ipp netaddr.IPPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
b.mu.Lock()
defer b.mu.Unlock()
n, ok = b.nodeByAddr[ipp.IP()]
n, ok = b.nodeByAddr[ipp.IP]
if !ok {
var ip netaddr.IP
if ipp.Port() != 0 {
if ipp.Port != 0 {
ip, ok = b.e.WhoIsIPPort(ipp)
}
if !ok {
@@ -453,13 +451,6 @@ 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
@@ -561,7 +552,7 @@ func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged
for _, peer := range nm.Peers {
for _, addr := range peer.Addresses {
if !addr.IsSingleIP() || addr.IP() != b.prefs.ExitNodeIP {
if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP {
continue
}
// Found the node being referenced, upgrade prefs to
@@ -657,12 +648,6 @@ 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:
@@ -716,7 +701,6 @@ 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
@@ -907,7 +891,7 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
}
if prefs != nil {
for _, r := range prefs.AdvertiseRoutes {
if r.Bits() == 0 {
if r.Bits == 0 {
// When offering a default route to the world, we
// filter out locally reachable LANs, so that the
// default route effectively appears to be a "guest
@@ -929,8 +913,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 {
@@ -975,20 +959,19 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{
func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
var b netaddr.IPSetBuilder
if err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) {
if tsaddr.IsTailscaleIP(pfx.IP()) {
if tsaddr.IsTailscaleIP(pfx.IP) {
return
}
if pfx.IsSingleIP() {
return
}
hostIPs = append(hostIPs, pfx.IP())
hostIPs = append(hostIPs, pfx.IP)
b.AddPrefix(pfx)
}); err != nil {
return nil, nil, err
}
ipSet, _ := b.IPSet()
return ipSet, hostIPs, nil
return b.IPSet(), hostIPs, nil
}
// shrinkDefaultRoute returns an IPSet representing the IPs in route,
@@ -1019,7 +1002,7 @@ func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
for _, pfx := range removeFromDefaultRoute {
b.RemovePrefix(pfx)
}
return b.IPSet()
return b.IPSet(), nil
}
// dnsCIDRsEqual determines whether two CIDR lists are equal
@@ -1711,45 +1694,9 @@ func (b *LocalBackend) authReconfig() {
rcfg := b.routerConfig(cfg, uc)
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)
}
var dcfg dns.Config
// If CorpDNS is false, dcfg remains the zero value.
if uc.CorpDNS {
addDefault := func(resolvers []tailcfg.DNSResolver) {
for _, resolver := range resolvers {
@@ -1763,6 +1710,9 @@ 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 {
@@ -1784,9 +1734,36 @@ 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"
for _, dom := range magicDNSRootDomains(nm) {
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
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)
}
}
@@ -1810,7 +1787,7 @@ func (b *LocalBackend) authReconfig() {
//
// https://github.com/tailscale/tailscale/issues/1713
addDefault(nm.DNS.FallbackResolvers)
case len(dcfg.Routes) == 0:
case len(dcfg.Routes) == 0 && len(dcfg.Hosts) == 0 && len(dcfg.AuthoritativeSuffixes) == 0:
// No settings requiring split DNS, no problem.
case version.OS() == "android":
// We don't support split DNS at all on Android yet.
@@ -1832,15 +1809,17 @@ func parseResolver(cfg tailcfg.DNSResolver) (netaddr.IPPort, error) {
if err != nil {
return netaddr.IPPort{}, fmt.Errorf("[unexpected] non-IP resolver %q", cfg.Addr)
}
return netaddr.IPPortFrom(ip, 53), nil
return netaddr.IPPort{
IP: ip,
Port: 53,
}, nil
}
// tailscaleVarRoot returns the root directory of Tailscale's writable
// storage area. (e.g. "/var/lib/tailscale")
func tailscaleVarRoot() string {
switch runtime.GOOS {
case "ios", "android":
dir, _ := paths.AppSharedDir.Load().(string)
if runtime.GOOS == "ios" {
dir, _ := paths.IOSSharedDir.Load().(string)
return dir
}
stateFile := paths.DefaultTailscaledStateFile()
@@ -1888,19 +1867,10 @@ 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 {
if pln.ip != b.netMap.Addresses[i].IP() {
if pln.ip != b.netMap.Addresses[i].IP {
allSame = false
break
}
@@ -1945,7 +1915,7 @@ func (b *LocalBackend) initPeerAPIListener() {
var err error
skipListen := i > 0 && isNetstack
if !skipListen {
ln, err = ps.listen(a.IP(), b.prevIfState)
ln, err = ps.listen(a.IP, b.prevIfState)
if err != nil {
if runtime.GOOS == "windows" {
// Expected for now. See Issue 1620.
@@ -1953,13 +1923,13 @@ 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
}
}
pln := &peerAPIListener{
ps: ps,
ip: a.IP(),
ip: a.IP,
ln: ln, // nil for 2nd+ on netstack
lb: b,
}
@@ -1968,7 +1938,7 @@ func (b *LocalBackend) initPeerAPIListener() {
} else {
pln.port = ln.Addr().(*net.TCPAddr).Port
}
pln.urlStr = "http://" + net.JoinHostPort(a.IP().String(), strconv.Itoa(pln.port))
pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.port))
b.logf("peerapi: serving on %s", pln.urlStr)
go pln.serve()
b.peerAPIListeners = append(b.peerAPIListeners, pln)
@@ -2019,14 +1989,14 @@ func peerRoutes(peers []wgcfg.Peer, cgnatThreshold int) (routes []netaddr.IPPref
for _, aip := range peer.AllowedIPs {
aip = unmapIPPrefix(aip)
// Only add the Tailscale IPv6 ULA once, if we see anybody using part of it.
if aip.IP().Is6() && aip.IsSingleIP() && tsULA.Contains(aip.IP()) {
if aip.IP.Is6() && aip.IsSingleIP() && tsULA.Contains(aip.IP) {
if !didULA {
didULA = true
routes = append(routes, tsULA)
}
continue
}
if aip.IsSingleIP() && cgNAT.Contains(aip.IP()) {
if aip.IsSingleIP() && cgNAT.Contains(aip.IP) {
cgNATIPs = append(cgNATIPs, aip)
} else {
routes = append(routes, aip)
@@ -2052,11 +2022,6 @@ 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
@@ -2098,13 +2063,16 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
}
}
rs.Routes = append(rs.Routes, netaddr.IPPrefixFrom(tsaddr.TailscaleServiceIP(), 32))
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
IP: tsaddr.TailscaleServiceIP(),
Bits: 32,
})
return rs
}
func unmapIPPrefix(ipp netaddr.IPPrefix) netaddr.IPPrefix {
return netaddr.IPPrefixFrom(ipp.IP().Unmap(), ipp.Bits())
return netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits}
}
func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
@@ -2188,7 +2156,7 @@ func (b *LocalBackend) enterState(newState ipn.State) {
case ipn.Running:
var addrs []string
for _, addr := range b.netMap.Addresses {
addrs = append(addrs, addr.IP().String())
addrs = append(addrs, addr.IP.String())
}
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrs, " "))
default:
@@ -2350,6 +2318,7 @@ 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{
@@ -2376,6 +2345,10 @@ 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
}
@@ -2451,7 +2424,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
addNode := func(n *tailcfg.Node) {
for _, ipp := range n.Addresses {
if ipp.IsSingleIP() {
b.nodeByAddr[ipp.IP()] = n
b.nodeByAddr[ipp.IP] = n
}
}
}
@@ -2577,42 +2550,6 @@ 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()
@@ -2639,9 +2576,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
continue
}
switch {
case a.IP().Is4():
case a.IP.Is4():
have4 = true
case a.IP().Is6():
case a.IP.Is6():
have6 = true
}
}
@@ -2657,11 +2594,11 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
var ipp netaddr.IPPort
switch {
case have4 && p4 != 0:
ipp = netaddr.IPPortFrom(nodeIP(peer, netaddr.IP.Is4), p4)
ipp = netaddr.IPPort{IP: nodeIP(peer, netaddr.IP.Is4), Port: p4}
case have6 && p6 != 0:
ipp = netaddr.IPPortFrom(nodeIP(peer, netaddr.IP.Is6), p6)
ipp = netaddr.IPPort{IP: nodeIP(peer, netaddr.IP.Is6), Port: p6}
}
if ipp.IP().IsZero() {
if ipp.IP.IsZero() {
return ""
}
return fmt.Sprintf("http://%v", ipp)
@@ -2669,8 +2606,8 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
func nodeIP(n *tailcfg.Node, pred func(netaddr.IP) bool) netaddr.IP {
for _, a := range n.Addresses {
if a.IsSingleIP() && pred(a.IP()) {
return a.IP()
if a.IsSingleIP() && pred(a.IP) {
return a.IP
}
}
return netaddr.IP{}

View File

@@ -171,7 +171,7 @@ func TestShrinkDefaultRoute(t *testing.T) {
out: []string{
"fe80::1",
"ff00::1",
tsaddr.TailscaleULARange().IP().String(),
tsaddr.TailscaleULARange().IP.String(),
},
localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is6() },
},

View File

@@ -510,7 +510,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
<body>
<h1>Hello, %s (%v)</h1>
This is my Tailscale device. Your device is %v.
`, html.EscapeString(who), h.remoteAddr.IP(), html.EscapeString(h.peerNode.ComputedName))
`, html.EscapeString(who), h.remoteAddr.IP, html.EscapeString(h.peerNode.ComputedName))
if h.isSelf {
fmt.Fprintf(w, "<p>You are the owner of this node.\n")

View File

@@ -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,ts_macext ios,ts_macext
// +build darwin,redo ios,redo
package ipnlocal

View File

@@ -140,8 +140,6 @@ 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)
}
@@ -248,10 +246,6 @@ 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.
@@ -554,7 +548,10 @@ 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))
c.Assert(nn[0].Prefs, 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?)
}
// undo the state hack above.
@@ -566,25 +563,24 @@ func TestStateMachine(t *testing.T) {
b.Logout()
{
nn := notifies.drain(2)
c.Assert([]string{"pause", "StartLogout"}, qt.DeepEquals, cc.getCalls())
// BUG: now is not the time to unpause.
c.Assert([]string{"unpause", "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.Stopped, qt.Equals, *nn[0].State)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
c.Assert(nn[1].Prefs.LoggedOut, qt.IsTrue)
c.Assert(nn[1].Prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.Stopped, qt.Equals, b.State())
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// Let's make the logout succeed.
t.Logf("\n\nLogout (async) - succeed")
notifies.expect(1)
notifies.expect(0)
cc.setAuthBlocked(true)
cc.send(nil, "", false, nil)
{
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)
notifies.drain(0)
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())

View File

@@ -147,7 +147,7 @@ func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
if err != nil {
return ci, fmt.Errorf("parsing local remote: %w", err)
}
if !la.IP().IsLoopback() || !ra.IP().IsLoopback() {
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
return ci, errors.New("non-loopback connection")
}
tab, err := netstat.Get()

View File

@@ -100,8 +100,6 @@ 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:
@@ -384,25 +382,6 @@ 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

View File

@@ -104,9 +104,7 @@ func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(Notify)) *
b: b,
sendNotifyMsg: sendNotifyMsg,
}
// b may be nil if the BackendServer is being created just to
// encapsulate and send an error message.
if sendNotifyMsg != nil && b != nil {
if sendNotifyMsg != nil {
b.SetNotifyCallback(bs.send)
}
return bs

View File

@@ -187,17 +187,3 @@ 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)
}
}

View File

@@ -15,7 +15,6 @@ import (
"net/http"
"os"
"strconv"
"sync/atomic"
"time"
"tailscale.com/logtail/backoff"
@@ -73,7 +72,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
}
l := &Logger{
stderr: cfg.Stderr,
stderrLevel: int64(cfg.StderrLevel),
stderrLevel: cfg.StderrLevel,
httpc: cfg.HTTPC,
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
lowMem: cfg.LowMemory,
@@ -104,7 +103,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
// logging facilities and uploading to a log server.
type Logger struct {
stderr io.Writer
stderrLevel int64 // accessed atomically
stderrLevel int
httpc *http.Client
url string
lowMem bool
@@ -126,8 +125,10 @@ 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) {
atomic.StoreInt64(&l.stderrLevel, int64(level))
l.stderrLevel = level
}
// SetLinkMonitor sets the optional the link monitor.
@@ -513,7 +514,7 @@ func (l *Logger) Write(buf []byte) (int, error) {
return 0, nil
}
level, buf := parseAndRemoveLogLevel(buf)
if l.stderr != nil && l.stderr != ioutil.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
if l.stderr != nil && l.stderr != ioutil.Discard && level <= l.stderrLevel {
if buf[len(buf)-1] == '\n' {
l.stderr.Write(buf)
} else {

View File

@@ -22,26 +22,27 @@ 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 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.
// Queries matching entries in Hosts are resolved locally without
// recursing off-machine.
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()
return c.hasDefaultResolvers() || c.hasRoutes() || c.hasHosts()
}
func (c Config) hasRoutes() bool {
@@ -51,7 +52,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()
return c.hasDefaultResolvers() && !c.hasRoutes() && !c.hasHosts()
}
func (c Config) hasDefaultResolvers() bool {
@@ -62,28 +63,44 @@ 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 (
prev []netaddr.IPPort
prevInitialized bool
)
var first []netaddr.IPPort
for _, resolvers := range c.Routes {
if !prevInitialized {
prev = resolvers
prevInitialized = true
if first == nil {
first = resolvers
continue
}
if !sameIPPorts(prev, resolvers) {
if !sameIPPorts(first, resolvers) {
return nil
}
}
return prev
return first
}
// matchDomains returns the list of match suffixes needed by Routes.
// 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.
func (c Config) matchDomains() []dnsname.FQDN {
ret := make([]dnsname.FQDN, 0, len(c.Routes))
for suffix := range c.Routes {
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
}
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()

View File

@@ -6,6 +6,7 @@ package dns
import (
"runtime"
"strings"
"time"
"inet.af/netaddr"
@@ -74,40 +75,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) (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
func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
// 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 rcfg, ocfg, nil
return resolver.Config{}, OSConfig{
SearchDomains: cfg.SearchDomains,
}, nil
case cfg.hasDefaultResolversOnly():
// Trivial CorpDNS configuration, just override the OS
// resolver.
ocfg.Nameservers = toIPsOnly(cfg.DefaultResolvers)
return rcfg, ocfg, nil
return resolver.Config{}, OSConfig{
Nameservers: toIPsOnly(cfg.DefaultResolvers),
SearchDomains: cfg.SearchDomains,
}, nil
case cfg.hasDefaultResolvers():
// Default resolvers plus other stuff always ends up proxying
// through quad-100.
rcfg.Routes = routes
rcfg.Routes["."] = cfg.DefaultResolvers
ocfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
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,
}
return rcfg, ocfg, nil
}
@@ -115,6 +116,8 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// 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
@@ -132,19 +135,35 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// This bool is used in a couple of places below to implement this
// workaround.
isWindows := runtime.GOOS == "windows"
if cfg.singleResolverSet() != nil && m.os.SupportsSplitDNS() && !isWindows {
// The windows check is for
// . See also below
// for further routing workarounds there.
if !cfg.hasHosts() && 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.
ocfg.Nameservers = toIPsOnly(cfg.singleResolverSet())
ocfg.MatchDomains = cfg.matchDomains()
return rcfg, ocfg, nil
return resolver.Config{}, OSConfig{
Nameservers: toIPsOnly(cfg.singleResolverSet()),
SearchDomains: cfg.SearchDomains,
MatchDomains: cfg.matchDomains(),
}, 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.Routes = routes
ocfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
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,
}
// If the OS can't do native split-dns, read out the underlying
// resolver config and blend it into our config.
@@ -154,7 +173,28 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
if !m.os.SupportsSplitDNS() || isWindows {
bcfg, err := m.os.GetBaseConfig()
if err != nil {
return resolver.Config{}, OSConfig{}, err
// 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)},
}
}
rcfg.Routes["."] = toIPPorts(bcfg.Nameservers)
ocfg.SearchDomains = append(ocfg.SearchDomains, bcfg.SearchDomains...)
@@ -171,7 +211,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
func toIPsOnly(ipps []netaddr.IPPort) (ret []netaddr.IP) {
ret = make([]netaddr.IP, 0, len(ipps))
for _, ipp := range ipps {
ret = append(ret, ipp.IP())
ret = append(ret, ipp.IP)
}
return ret
}
@@ -179,7 +219,7 @@ func toIPsOnly(ipps []netaddr.IPPort) (ret []netaddr.IP) {
func toIPPorts(ips []netaddr.IP) (ret []netaddr.IPPort) {
ret = make([]netaddr.IPPort, 0, len(ips))
for _, ip := range ips {
ret = append(ret, netaddr.IPPortFrom(ip, 53))
ret = append(ret, netaddr.IPPort{IP: ip, Port: 53})
}
return ret
}

View File

@@ -76,20 +76,6 @@ 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{
@@ -118,10 +104,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"),
@@ -140,10 +126,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{
@@ -275,8 +261,8 @@ func TestManager(t *testing.T) {
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
Routes: upstreams("ts.com", ""),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"),
@@ -300,8 +286,8 @@ func TestManager(t *testing.T) {
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
Routes: upstreams("ts.com", ""),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
@@ -319,11 +305,12 @@ func TestManager(t *testing.T) {
{
name: "routes-magic",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53", "ts.com", ""),
Routes: upstreams("corp.com", "2.2.2.2:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"),
@@ -346,13 +333,12 @@ func TestManager(t *testing.T) {
{
name: "routes-magic-split",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"ts.com", ""),
Routes: upstreams("corp.com", "2.2.2.2:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
@@ -382,12 +368,11 @@ func TestManager(t *testing.T) {
if err := m.Set(test.in); err != nil {
t.Fatalf("m.Set: %v", err)
}
trIP := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
trIPPort := cmp.Transformer("ippStr", func(ipp netaddr.IPPort) string { return ipp.String() })
if diff := cmp.Diff(f.OSConfig, test.os, trIP, trIPPort, cmpopts.EquateEmpty()); diff != "" {
tr := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
if diff := cmp.Diff(f.OSConfig, test.os, tr, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("wrong OSConfig (-got+want)\n%s", diff)
}
if diff := cmp.Diff(f.ResolverConfig, test.rs, trIP, trIPPort, cmpopts.EquateEmpty()); diff != "" {
if diff := cmp.Diff(f.ResolverConfig, test.rs, tr, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("wrong resolver.Config (-got+want)\n%s", diff)
}
})
@@ -443,12 +428,7 @@ 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 s == "" {
if key == "" {
panic("IPPort provided before suffix")
}
ret[key] = nil
} else if ipp, err := netaddr.ParseIPPort(s); err == nil {
if ipp, err := netaddr.ParseIPPort(s); err == nil {
if key == "" {
panic("IPPort provided before suffix")
}

View File

@@ -14,7 +14,6 @@ import (
"math/rand"
"net"
"sync"
"syscall"
"time"
dns "golang.org/x/net/dns/dnsmessage"
@@ -194,14 +193,8 @@ func (f *forwarder) recv(conn *fwdConn) {
return
default:
}
// The 1 extra byte is to detect packet truncation.
out := make([]byte, maxResponseBytes+1)
out := make([]byte, maxResponseBytes)
n := conn.read(out)
var truncated bool
if n > maxResponseBytes {
n = maxResponseBytes
truncated = true
}
if n == 0 {
continue
}
@@ -212,19 +205,6 @@ 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]
@@ -306,8 +286,6 @@ func (f *forwarder) forward(query packet) error {
}
f.mu.Unlock()
// TODO(#2066): EDNS size clamping
for _, resolver := range resolvers {
f.send(query.bs, resolver)
}
@@ -393,12 +371,6 @@ 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")
@@ -450,7 +422,7 @@ func (c *fwdConn) read(out []byte) int {
c.mu.Unlock()
n, _, err := conn.ReadFrom(out)
if err == nil || packetWasTruncated(err) {
if err == nil {
// Success.
return n
}

View File

@@ -23,8 +23,3 @@ 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 }

View File

@@ -8,8 +8,3 @@ 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 }

View File

@@ -5,7 +5,6 @@
package resolver
import (
"errors"
"net"
"os"
@@ -28,16 +27,3 @@ 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)
}

View File

@@ -22,10 +22,8 @@ import (
"tailscale.com/wgengine/monitor"
)
// 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
// maxResponseBytes is the maximum size of a response from a Resolver.
const maxResponseBytes = 512
// queueSize is the maximal number of DNS requests that can await polling.
// If EnqueueRequest is called when this many requests are already pending,

View File

@@ -66,39 +66,6 @@ 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)

View File

@@ -6,9 +6,7 @@ package resolver
import (
"bytes"
"encoding/hex"
"errors"
"math/rand"
"net"
"testing"
@@ -46,11 +44,9 @@ func dnspacket(domain dnsname.FQDN, tp dns.Type) []byte {
}
type dnsResponse struct {
ip netaddr.IP
txt []string
name dnsname.FQDN
rcode dns.RCode
truncated bool
ip netaddr.IP
name dnsname.FQDN
rcode dns.RCode
}
func unpackResponse(payload []byte) (dnsResponse, error) {
@@ -71,16 +67,6 @@ 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
@@ -104,12 +90,6 @@ 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 {
@@ -289,32 +269,6 @@ 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)
@@ -322,43 +276,13 @@ 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,
"small.txt.", resolveToTXT(smallTXT),
"med.txt.", resolveToTXT(medTXT),
"large.txt.", resolveToTXT(largeTXT),
"xlarge.txt.", resolveToTXT(xlargeTXT),
"huge.txt.", resolveToTXT(hugeTXT))
"nxdomain.site.", resolveToNXDOMAIN)
defer v4server.Shutdown()
v6server := serveDNS(t, "[::1]:0",
"test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."),
"nxdomain.site.", resolveToNXDOMAIN,
"small.txt.", resolveToTXT(smallTXT),
"med.txt.", resolveToTXT(medTXT),
"large.txt.", resolveToTXT(largeTXT),
"xlarge.txt.", resolveToTXT(xlargeTXT),
"huge.txt.", resolveToTXT(hugeTXT))
"nxdomain.site.", resolveToNXDOMAIN)
defer v6server.Shutdown()
r := New(t.Logf, nil)
@@ -398,31 +322,6 @@ 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 {
@@ -446,15 +345,6 @@ 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])
}
}
}
})
}
}
@@ -543,8 +433,8 @@ func TestDelegateCollision(t *testing.T) {
qtype dns.Type
addr netaddr.IPPort
}{
{"test.site.", dns.TypeA, netaddr.IPPortFrom(netaddr.IPv4(1, 1, 1, 1), 1001)},
{"test.site.", dns.TypeAAAA, netaddr.IPPortFrom(netaddr.IPv4(1, 1, 1, 1), 1002)},
{"test.site.", dns.TypeA, netaddr.IPPort{IP: netaddr.IPv4(1, 1, 1, 1), Port: 1001}},
{"test.site.", dns.TypeAAAA, netaddr.IPPort{IP: netaddr.IPv4(1, 1, 1, 1), Port: 1002}},
}
// packets will have the same dns txid.

View File

@@ -195,7 +195,7 @@ func ForeachInterface(fn func(Interface, []netaddr.IPPrefix)) error {
}
}
sort.Slice(pfxs, func(i, j int) bool {
return pfxs[i].IP().Less(pfxs[j].IP())
return pfxs[i].IP.Less(pfxs[j].IP)
})
fn(Interface{iface}, pfxs)
}
@@ -264,7 +264,7 @@ func (s *State) String() string {
fmt.Fprintf(&sb, "%s:[", ifName)
needSpace := false
for _, pfx := range s.InterfaceIPs[ifName] {
if !isInterestingIP(pfx.IP()) {
if !isInterestingIP(pfx.IP) {
continue
}
if needSpace {
@@ -367,7 +367,7 @@ func (s *State) AnyInterfaceUp() bool {
func hasTailscaleIP(pfxs []netaddr.IPPrefix) bool {
for _, pfx := range pfxs {
if tsaddr.IsTailscaleIP(pfx.IP()) {
if tsaddr.IsTailscaleIP(pfx.IP) {
return true
}
}
@@ -407,11 +407,11 @@ func GetState() (*State, error) {
return
}
for _, pfx := range pfxs {
if pfx.IP().IsLoopback() || pfx.IP().IsLinkLocalUnicast() {
if pfx.IP.IsLoopback() || pfx.IP.IsLinkLocalUnicast() {
continue
}
s.HaveV6Global = s.HaveV6Global || isGlobalV6(pfx.IP())
s.HaveV4 = s.HaveV4 || pfx.IP().Is4()
s.HaveV6Global = s.HaveV6Global || isGlobalV6(pfx.IP)
s.HaveV4 = s.HaveV4 || pfx.IP.Is4()
}
}); err != nil {
return nil, err
@@ -447,7 +447,7 @@ func HTTPOfListener(ln net.Listener) string {
var goodIP string
var privateIP string
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
ip := pfx.IP()
ip := pfx.IP
if isPrivateIP(ip) {
if privateIP == "" {
privateIP = ip.String()
@@ -484,7 +484,7 @@ func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) {
return
}
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
ip := pfx.IP()
ip := pfx.IP
if !i.IsUp() || ip.IsZero() || !myIP.IsZero() {
return
}
@@ -528,7 +528,7 @@ var (
// isInterestingIP.
func anyInterestingIP(pfxs []netaddr.IPPrefix) bool {
for _, pfx := range pfxs {
if isInterestingIP(pfx.IP()) {
if isInterestingIP(pfx.IP) {
return true
}
}

View File

@@ -2,11 +2,15 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux darwin,!ts_macext
// +build linux,!redo
package interfaces
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
@@ -19,3 +23,64 @@ 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)
}
}

View File

@@ -4,74 +4,7 @@
package interfaces
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)
}
}
import "testing"
func BenchmarkDefaultRouteInterface(b *testing.B) {
b.ReportAllocs()

View File

@@ -625,7 +625,7 @@ func (rs *reportState) stopTimers() {
func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort, d time.Duration) {
var ipPortStr string
if ipp != (netaddr.IPPort{}) {
ipPortStr = net.JoinHostPort(ipp.IP().String(), fmt.Sprint(ipp.Port()))
ipPortStr = net.JoinHostPort(ipp.IP.String(), fmt.Sprint(ipp.Port))
}
rs.mu.Lock()
@@ -650,13 +650,13 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort
}
switch {
case ipp.IP().Is6():
case ipp.IP.Is6():
updateLatency(ret.RegionV6Latency, node.RegionID, d)
ret.IPv6 = true
ret.GlobalV6 = ipPortStr
// TODO: track MappingVariesByDestIP for IPv6
// too? Would be sad if so, but who knows.
case ipp.IP().Is4():
case ipp.IP.Is4():
updateLatency(ret.RegionV4Latency, node.RegionID, d)
ret.IPv4 = true
if rs.gotEP4 == "" {
@@ -1172,7 +1172,7 @@ func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeP
if proto == probeIPv6 && ip.Is4() {
return nil
}
return netaddr.IPPortFrom(ip, uint16(port)).UDPAddr()
return netaddr.IPPort{IP: ip, Port: uint16(port)}.UDPAddr()
}
switch proto {
@@ -1182,7 +1182,7 @@ func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeP
if !ip.Is4() {
return nil
}
return netaddr.IPPortFrom(ip, uint16(port)).UDPAddr()
return netaddr.IPPort{IP: ip, Port: uint16(port)}.UDPAddr()
}
case probeIPv6:
if n.IPv6 != "" {
@@ -1190,7 +1190,7 @@ func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeP
if !ip.Is6() {
return nil
}
return netaddr.IPPortFrom(ip, uint16(port)).UDPAddr()
return netaddr.IPPort{IP: ip, Port: uint16(port)}.UDPAddr()
}
default:
return nil

View File

@@ -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,!ts_macext
// +build darwin,!redo
package netns

View File

@@ -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,ts_macext
// +build !linux,!windows,!darwin darwin,redo
package netns

View File

@@ -157,9 +157,10 @@ func ipport4(addr uint32, port uint16) netaddr.IPPort {
if !endian.Big {
addr = bits.ReverseBytes32(addr)
}
return netaddr.IPPortFrom(
netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
port)
return netaddr.IPPort{
IP: netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
Port: port,
}
}
func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
@@ -168,7 +169,10 @@ func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
// TODO: something better here?
ip = ip.WithZone(fmt.Sprint(scope))
}
return netaddr.IPPortFrom(ip, port)
return netaddr.IPPort{
IP: ip,
Port: port,
}
}
func port(v *uint32) uint16 {

View File

@@ -12,6 +12,7 @@ import (
"inet.af/netaddr"
"tailscale.com/types/ipproto"
"tailscale.com/types/strbuilder"
)
const unknown = ipproto.Unknown
@@ -61,17 +62,36 @@ func (p *Parsed) String() string {
return "Unknown{???}"
}
// 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)
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))
}
// Decode extracts data from the packet in b into q.
@@ -122,8 +142,8 @@ func (q *Parsed) decode4(b []byte) {
}
// If it's valid IPv4, then the IP addresses are valid
q.Src = q.Src.WithIP(netaddr.IPv4(b[12], b[13], b[14], b[15]))
q.Dst = q.Dst.WithIP(netaddr.IPv4(b[16], b[17], b[18], b[19]))
q.Src.IP = netaddr.IPv4(b[12], b[13], b[14], b[15])
q.Dst.IP = netaddr.IPv4(b[16], b[17], b[18], b[19])
q.subofs = int((b[0] & 0x0F) << 2)
if q.subofs > q.length {
@@ -165,8 +185,8 @@ func (q *Parsed) decode4(b []byte) {
q.IPProto = unknown
return
}
q.Src = q.Src.WithPort(0)
q.Dst = q.Dst.WithPort(0)
q.Src.Port = 0
q.Dst.Port = 0
q.dataofs = q.subofs + icmp4HeaderLength
return
case ipproto.IGMP:
@@ -178,8 +198,8 @@ func (q *Parsed) decode4(b []byte) {
q.IPProto = unknown
return
}
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
headerLength := (sub[12] & 0xF0) >> 2
q.dataofs = q.subofs + int(headerLength)
@@ -189,8 +209,8 @@ func (q *Parsed) decode4(b []byte) {
q.IPProto = unknown
return
}
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.dataofs = q.subofs + udpHeaderLength
return
case ipproto.SCTP:
@@ -198,8 +218,8 @@ func (q *Parsed) decode4(b []byte) {
q.IPProto = unknown
return
}
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
return
case ipproto.TSMP:
// Inter-tailscale messages.
@@ -245,10 +265,8 @@ func (q *Parsed) decode6(b []byte) {
// okay to ignore `ok` here, because IPs pulled from packets are
// always well-formed stdlib IPs.
srcIP, _ := netaddr.FromStdIP(net.IP(b[8:24]))
dstIP, _ := netaddr.FromStdIP(net.IP(b[24:40]))
q.Src = q.Src.WithIP(srcIP)
q.Dst = q.Dst.WithIP(dstIP)
q.Src.IP, _ = netaddr.FromStdIP(net.IP(b[8:24]))
q.Dst.IP, _ = netaddr.FromStdIP(net.IP(b[24:40]))
// We don't support any IPv6 extension headers. Don't try to
// be clever. Therefore, the IP subprotocol always starts at
@@ -272,16 +290,16 @@ func (q *Parsed) decode6(b []byte) {
q.IPProto = unknown
return
}
q.Src = q.Src.WithPort(0)
q.Dst = q.Dst.WithPort(0)
q.Src.Port = 0
q.Dst.Port = 0
q.dataofs = q.subofs + icmp6HeaderLength
case ipproto.TCP:
if len(sub) < tcpHeaderLength {
q.IPProto = unknown
return
}
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
headerLength := (sub[12] & 0xF0) >> 2
q.dataofs = q.subofs + int(headerLength)
@@ -291,16 +309,16 @@ func (q *Parsed) decode6(b []byte) {
q.IPProto = unknown
return
}
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.dataofs = q.subofs + udpHeaderLength
case ipproto.SCTP:
if len(sub) < sctpHeaderLength {
q.IPProto = unknown
return
}
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
return
case ipproto.TSMP:
// Inter-tailscale messages.
@@ -320,8 +338,8 @@ func (q *Parsed) IP4Header() IP4Header {
return IP4Header{
IPID: ipid,
IPProto: q.IPProto,
Src: q.Src.IP(),
Dst: q.Dst.IP(),
Src: q.Src.IP,
Dst: q.Dst.IP,
}
}
@@ -333,8 +351,8 @@ func (q *Parsed) IP6Header() IP6Header {
return IP6Header{
IPID: ipid,
IPProto: q.IPProto,
Src: q.Src.IP(),
Dst: q.Dst.IP(),
Src: q.Src.IP,
Dst: q.Dst.IP,
}
}
@@ -355,8 +373,8 @@ func (q *Parsed) UDP4Header() UDP4Header {
}
return UDP4Header{
IP4Header: q.IP4Header(),
SrcPort: q.Src.Port(),
DstPort: q.Dst.Port(),
SrcPort: q.Src.Port,
DstPort: q.Dst.Port,
}
}

View File

@@ -378,9 +378,11 @@ func TestParsedString(t *testing.T) {
})
}
var sink string
allocs := testing.AllocsPerRun(1000, func() {
sinkString = tests[0].qdecode.String()
sink = tests[0].qdecode.String()
})
_ = sink
if allocs != 1 {
t.Errorf("allocs = %v; want 1", allocs)
}
@@ -530,33 +532,3 @@ 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()
}
})
}
}

View File

@@ -143,7 +143,7 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
if len(buf) > maxPacketLength {
return errLargePacket
}
if h.Src.IP().Is4() {
if h.Src.IP.Is4() {
iph := IP4Header{
IPProto: ipproto.TSMP,
Src: h.IPSrc,
@@ -151,7 +151,7 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
}
iph.Marshal(buf)
buf = buf[ip4HeaderLength:]
} else if h.Src.IP().Is6() {
} else if h.Src.IP.Is6() {
iph := IP6Header{
IPProto: ipproto.TSMP,
Src: h.IPSrc,
@@ -165,8 +165,8 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
buf[0] = byte(TSMPTypeRejectedConn)
buf[1] = byte(h.Proto)
buf[2] = byte(h.Reason)
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port())
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port())
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
if h.hasFlags() {
var flags byte
@@ -190,10 +190,10 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo
h = TailscaleRejectedHeader{
Proto: ipproto.Proto(p[1]),
Reason: TailscaleRejectReason(p[2]),
IPSrc: pp.Src.IP(),
IPDst: pp.Dst.IP(),
Src: netaddr.IPPortFrom(pp.Dst.IP(), binary.BigEndian.Uint16(p[3:5])),
Dst: netaddr.IPPortFrom(pp.Src.IP(), binary.BigEndian.Uint16(p[5:7])),
IPSrc: pp.Src.IP,
IPDst: pp.Dst.IP,
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
}
if len(p) > 7 {
flags := p[7]

View File

@@ -84,7 +84,7 @@ type pmpMapping struct {
// externalValid reports whether m.external is valid, with both its IP and Port populated.
func (m *pmpMapping) externalValid() bool {
return !m.external.IP().IsZero() && m.external.Port() != 0
return !m.external.IP.IsZero() && m.external.Port != 0
}
// release does a best effort fire-and-forget release of the PMP mapping m.
@@ -94,8 +94,8 @@ func (m *pmpMapping) release() {
return
}
defer uc.Close()
pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
uc.WriteTo(pkt, netaddr.IPPortFrom(m.gw, pmpPort).UDPAddr())
pkt := buildPMPRequestMappingPacket(m.internal.Port, m.external.Port, pmpMapLifetimeDelete)
uc.WriteTo(pkt, netaddr.IPPort{IP: m.gw, Port: pmpPort}.UDPAddr())
}
// NewClient returns a new portmapping client.
@@ -256,7 +256,7 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
localPort := c.localPort
m := &pmpMapping{
gw: gw,
internal: netaddr.IPPortFrom(myIP, localPort),
internal: netaddr.IPPort{IP: myIP, Port: localPort},
}
// prevPort is the port we had most previously, if any. We try
@@ -271,7 +271,7 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
return m.external, nil
}
// The mapping might still be valid, so just try to renew it.
prevPort = m.external.Port()
prevPort = m.external.Port
}
// If we just did a Probe (e.g. via netchecker) but didn't
@@ -279,7 +279,7 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
// again. Cuts down latency for most clients.
haveRecentPMP := c.sawPMPRecentlyLocked()
if haveRecentPMP {
m.external = m.external.WithIP(c.pmpPubIP)
m.external.IP = c.pmpPubIP
}
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
c.mu.Unlock()
@@ -297,11 +297,11 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
defer closeCloserOnContextDone(ctx, uc)()
pmpAddr := netaddr.IPPortFrom(gw, pmpPort)
pmpAddr := netaddr.IPPort{IP: gw, Port: pmpPort}
pmpAddru := pmpAddr.UDPAddr()
// Ask for our external address if needed.
if m.external.IP().IsZero() {
if m.external.IP.IsZero() {
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil {
return netaddr.IPPort{}, err
}
@@ -337,10 +337,10 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
}
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
m.external = m.external.WithIP(pres.PublicAddr)
m.external.IP = pres.PublicAddr
}
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
m.external = m.external.WithPort(pres.ExternalPort)
m.external.Port = pres.ExternalPort
d := time.Duration(pres.MappingValidSeconds) * time.Second
d /= 2 // renew in half the time
m.useUntil = time.Now().Add(d)
@@ -468,9 +468,9 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
defer cancel()
defer closeCloserOnContextDone(ctx, uc)()
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
upnpAddr := netaddr.IPPortFrom(gw, upnpPort).UDPAddr()
pcpAddr := netaddr.IPPort{IP: gw, Port: pcpPort}.UDPAddr()
pmpAddr := netaddr.IPPort{IP: gw, Port: pmpPort}.UDPAddr()
upnpAddr := netaddr.IPPort{IP: gw, Port: upnpPort}.UDPAddr()
// Don't send probes to services that we recently learned (for
// the same gw/myIP) are available. See

View File

@@ -41,9 +41,12 @@ var (
// TailscaleServiceIP returns the listen address of services
// provided by Tailscale itself such as the MagicDNS proxy.
func TailscaleServiceIP() netaddr.IP {
return netaddr.IPv4(100, 100, 100, 100) // "100.100.100.100" for those grepping
serviceIP.Do(func() { mustIP(&serviceIP.v, "100.100.100.100") })
return serviceIP.v
}
var serviceIP onceIP
// IsTailscaleIP reports whether ip is an IP address in a range that
// Tailscale assigns from.
func IsTailscaleIP(ip netaddr.IP) bool {
@@ -89,7 +92,7 @@ func TailscaleEphemeral6Range() netaddr.IPPrefix {
// Currently used to work around a Windows limitation when programming
// IPv6 routes in corner cases.
func Tailscale4To6Placeholder() netaddr.IP {
return Tailscale4To6Range().IP()
return Tailscale4To6Range().IP
}
// Tailscale4To6 returns a Tailscale IPv6 address that maps 1:1 to the
@@ -99,7 +102,7 @@ func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
if !ipv4.Is4() || !IsTailscaleIP(ipv4) {
return netaddr.IP{}
}
ret := Tailscale4To6Range().IP().As16()
ret := Tailscale4To6Range().IP.As16()
v4 := ipv4.As4()
copy(ret[13:], v4[1:])
return netaddr.IPFrom16(ret)
@@ -123,6 +126,19 @@ 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
@@ -156,16 +172,16 @@ func NewContainsIPFunc(addrs []netaddr.IPPrefix) func(ip netaddr.IP) bool {
// Fast paths for 1 and 2 IPs:
if len(addrs) == 1 {
a := addrs[0]
return func(ip netaddr.IP) bool { return ip == a.IP() }
return func(ip netaddr.IP) bool { return ip == a.IP }
}
if len(addrs) == 2 {
a, b := addrs[0], addrs[1]
return func(ip netaddr.IP) bool { return ip == a.IP() || ip == b.IP() }
return func(ip netaddr.IP) bool { return ip == a.IP || ip == b.IP }
}
// General case:
m := map[netaddr.IP]bool{}
for _, a := range addrs {
m[a.IP()] = true
m[a.IP] = true
}
return func(ip netaddr.IP) bool { return m[ip] }
}

View File

@@ -93,11 +93,3 @@ 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()
}
}

View File

@@ -8,7 +8,7 @@ import (
"io"
"os"
"golang.zx2c4.com/wireguard/tun"
"github.com/tailscale/wireguard-go/tun"
)
type fakeTUN struct {

View File

@@ -9,7 +9,7 @@ package tstun
import (
"time"
"golang.zx2c4.com/wireguard/tun"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/types/logger"
)

View File

@@ -9,7 +9,7 @@ import (
"sync"
"time"
"golang.zx2c4.com/wireguard/tun"
"github.com/tailscale/wireguard-go/tun"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/types/logger"
)

View File

@@ -11,34 +11,27 @@ import (
"os"
"os/exec"
"runtime"
"strconv"
"time"
"golang.zx2c4.com/wireguard/tun"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/types/logger"
"tailscale.com/version/distro"
)
// 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?!).
// 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?!).
//
// 1280 is the smallest MTU allowed for IPv6, which is a sensible
// "probably works everywhere" setting until we develop proper PMTU
// discovery.
var tunMTU = 1280
func init() {
if mtu, _ := strconv.Atoi(os.Getenv("TS_DEBUG_MTU")); mtu != 0 {
tunMTU = mtu
}
}
const minimalMTU = 1280
// 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, tunMTU)
dev, err := tun.CreateTUN(tunName, minimalMTU)
if err != nil {
return nil, "", err
}

View File

@@ -6,7 +6,7 @@
package tstun
import "golang.zx2c4.com/wireguard/tun"
import "github.com/tailscale/wireguard-go/tun"
func interfaceName(dev tun.Device) (string, error) {
return dev.Name()

View File

@@ -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"
)

View File

@@ -14,8 +14,8 @@ import (
"sync/atomic"
"time"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
"tailscale.com/net/packet"
"tailscale.com/types/ipproto"
@@ -352,7 +352,7 @@ func (t *Wrapper) Read(buf []byte, offset int) (int, error) {
p.Decode(buf[offset : offset+n])
if m, ok := t.destIPActivity.Load().(map[netaddr.IP]func()); ok {
if fn := m[p.Dst.IP()]; fn != nil {
if fn := m[p.Dst.IP]; fn != nil {
fn()
}
}
@@ -412,7 +412,7 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
p.IPProto == ipproto.TCP &&
p.TCPFlags&packet.TCPSyn != 0 &&
t.PeerAPIPort != nil {
if port, ok := t.PeerAPIPort(p.Dst.IP()); ok && port == p.Dst.Port() {
if port, ok := t.PeerAPIPort(p.Dst.IP); ok && port == p.Dst.Port {
outcome = filter.Accept
}
}
@@ -425,8 +425,8 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
// can show them a rejection history with reasons.
if p.IPVersion == 4 && p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 && !t.disableTSMPRejected {
rj := packet.TailscaleRejectedHeader{
IPSrc: p.Dst.IP(),
IPDst: p.Src.IP(),
IPSrc: p.Dst.IP,
IPDst: p.Src.IP,
Src: p.Src,
Dst: p.Dst,
Proto: p.IPProto,
@@ -536,7 +536,7 @@ func (t *Wrapper) injectOutboundPong(pp *packet.Parsed, req packet.TSMPPingReque
Data: req.Data,
}
if t.PeerAPIPort != nil {
pong.PeerAPIPort, _ = t.PeerAPIPort(pp.Dst.IP())
pong.PeerAPIPort, _ = t.PeerAPIPort(pp.Dst.IP)
}
switch pp.IPVersion {
case 4:

View File

@@ -14,7 +14,7 @@ import (
"testing"
"unsafe"
"golang.zx2c4.com/wireguard/tun/tuntest"
"github.com/tailscale/wireguard-go/tun/tuntest"
"inet.af/netaddr"
"tailscale.com/net/packet"
"tailscale.com/types/ipproto"
@@ -82,7 +82,7 @@ func nets(nets ...string) (ret []netaddr.IPPrefix) {
if ip.Is6() {
bits = 128
}
ret = append(ret, netaddr.IPPrefixFrom(ip, bits))
ret = append(ret, netaddr.IPPrefix{IP: ip, Bits: bits})
} else {
pfx, err := netaddr.ParseIPPrefix(s)
if err != nil {
@@ -146,8 +146,7 @@ func setfilter(logf logger.Logf, tun *Wrapper) {
}
var sb netaddr.IPSetBuilder
sb.AddPrefix(netaddr.MustParseIPPrefix("1.2.0.0/16"))
ipSet, _ := sb.IPSet()
tun.SetFilter(filter.New(matches, ipSet, ipSet, nil, logf))
tun.SetFilter(filter.New(matches, sb.IPSet(), sb.IPSet(), nil, logf))
}
func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper) {

View File

@@ -1,184 +0,0 @@
// 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
}
}
}

View File

@@ -1,202 +0,0 @@
// 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)
}

View File

@@ -11,13 +11,11 @@ import (
"path/filepath"
"runtime"
"sync/atomic"
"tailscale.com/version/distro"
)
// AppSharedDir is a string set by the iOS or Android app on start
// IOSSharedDir is a string set by the iOS app on start
// containing a directory we can read/write in.
var AppSharedDir atomic.Value
var IOSSharedDir atomic.Value
// DefaultTailscaledSocket returns the path to the tailscaled Unix socket
// or the empty string if there's no reasonable default.
@@ -28,15 +26,11 @@ func DefaultTailscaledSocket() string {
if runtime.GOOS == "darwin" {
return "/var/run/tailscaled.socket"
}
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 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 fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() {

View File

@@ -8,6 +8,8 @@
set -eu
RELEASE="${TAILSCALE_RELEASE:-stable}"
# All the code is wrapped in a main function that gets called at the
# bottom of the file, so that a truncated partial download doesn't end
# up executing half a script.
@@ -91,7 +93,7 @@ main() {
;;
alpine)
OS="$ID"
VERSION="$VERSION_ID"
VERSION="$(echo $PRETTY_NAME | cut -d' ' -f3)"
PACKAGETYPE="apk"
;;
nixos)
@@ -182,6 +184,10 @@ main() {
OS_UNSUPPORTED=1
fi
;;
fedora)
# We support every fedora release currently in use.
# No checking needed.
;;
centos)
if [ "$VERSION" != "7" ] && \
[ "$VERSION" != "8" ]
@@ -216,8 +222,10 @@ main() {
# Rolling release, no version checking needed.
;;
alpine)
# All versions supported, no version checking needed.
# TODO: is that true? When was tailscale packaged?
if [ "$VERSION" != "edge" ]
then
OS_UNSUPPORTED=1
fi
;;
void)
# Rolling release, no version checking needed.
@@ -325,38 +333,39 @@ main() {
# TODO: use newfangled per-repo signature scheme
set -x
$CURL "https://pkgs.tailscale.com/stable/$OS/$VERSION.gpg" | $SUDO apt-key add -
$CURL "https://pkgs.tailscale.com/stable/$OS/$VERSION.list" | $SUDO tee /etc/apt/sources.list.d/tailscale.list
$CURL "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION.gpg" | $SUDO apt-key add -
$CURL "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION.list" | $SUDO tee /etc/apt/sources.list.d/tailscale.list
$SUDO apt-get update
$SUDO apt-get install tailscale
set +x
;;
yum)
set -x
$SUDO yum install yum-utils
$SUDO yum-config-manager --add-repo "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
$SUDO yum install tailscale
$SUDO yum install -y yum-utils
$SUDO yum-config-manager --add-repo "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION/tailscale.repo"
$SUDO yum install -y tailscale
$SUDO systemctl enable --now tailscaled
set +x
;;
dnf)
set -x
$SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
$SUDO dnf install tailscale
$SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION/tailscale.repo"
$SUDO dnf install -y tailscale
$SUDO systemctl enable --now tailscaled
set +x
;;
zypper)
set -x
$SUDO zypper ar -g -r "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
$SUDO zypper ref
$SUDO zypper in tailscale
$SUDO rpm --import https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION/repo.gpg
$SUDO zypper ar -g -r "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION/tailscale.repo"
$SUDO zypper ref -r tailscale-stable
$SUDO zypper in -y tailscale
$SUDO systemctl enable --now tailscaled
set +x
;;
pacman)
set -x
$SUDO pacman -S tailscale
$SUDO pacman -S --noconfirm tailscale
$SUDO systemctl enable --now tailscaled
set +x
;;
@@ -364,6 +373,7 @@ main() {
set -x
$SUDO apk add tailscale
$SUDO rc-update add tailscale
$SUDO service tailscale start
set +x
;;
xbps)
@@ -381,6 +391,11 @@ main() {
open "https://apps.apple.com/us/app/tailscale/id1475387142"
set +x
;;
pkg)
set -x
$SUDO pkg install -y tailscale
set +x
;;
*)
echo "unexpected: unknown package type $PACKAGETYPE"
exit 1

View File

@@ -1026,16 +1026,11 @@ 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 appendKey(base []byte, prefix string, k [32]byte) []byte {
ret := append(base, make([]byte, len(prefix)+64)...)
buf := ret[len(base):]
func keyMarshalText(prefix string, k [32]byte) []byte {
buf := make([]byte, len(prefix)+64)
copy(buf, prefix)
hex.Encode(buf[len(prefix):], k[:])
return ret
}
func keyMarshalText(prefix string, k [32]byte) []byte {
return appendKey(nil, prefix, k)
return buf
}
func keyUnmarshalText(dst []byte, prefix string, text []byte) error {
@@ -1066,7 +1061,6 @@ 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{} }
@@ -1174,31 +1168,3 @@ 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
}

View File

@@ -14,7 +14,6 @@ import (
"inet.af/netaddr"
"tailscale.com/types/wgkey"
"tailscale.com/version"
)
func fieldsOf(t reflect.Type) (fields []string) {
@@ -529,25 +528,3 @@ 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)
}
}

View File

@@ -70,7 +70,7 @@ func (s *Server) WhoIs(addr string) (w *apitype.WhoIsResponse, ok bool) {
if err != nil {
return nil, false
}
ipp = ipp.WithIP(ip)
ipp.IP = ip
}
n, up, ok := s.lb.WhoIs(ipp)
if !ok {

View File

@@ -1,100 +0,0 @@
// 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 ""
}

View File

@@ -2,6 +2,7 @@
// 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 (
@@ -20,6 +21,7 @@ import (
"net/http/httptest"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
@@ -42,6 +44,7 @@ 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")
@@ -62,7 +65,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()
@@ -104,7 +107,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()
@@ -151,7 +154,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()
@@ -193,88 +196,23 @@ func TestTwoNodes(t *testing.T) {
d2.MustCleanShutdown(t)
}
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)
// 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 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")
// 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()),
}
}
@@ -282,7 +220,7 @@ func TestAddPingRequest(t *testing.T) {
// or more nodes.
type testEnv struct {
t testing.TB
Binaries *Binaries
Binaries *testBinaries
LogCatcher *logCatcher
LogCatcherServer *httptest.Server
@@ -300,7 +238,7 @@ type testEnv struct {
// environment.
//
// Call Close to shut everything down.
func newTestEnv(t testing.TB, bins *Binaries) *testEnv {
func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
if runtime.GOOS == "windows" {
t.Skip("not tested/working on Windows yet")
}
@@ -383,7 +321,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,
@@ -461,7 +399,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
@@ -488,6 +426,63 @@ 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

View File

@@ -54,39 +54,6 @@ 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 {
@@ -198,13 +165,6 @@ 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()
}
@@ -347,10 +307,6 @@ 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))),
@@ -358,8 +314,6 @@ 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] {
@@ -402,9 +356,6 @@ 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) {
@@ -586,14 +537,6 @@ 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
}
@@ -707,7 +650,7 @@ func keepClientEndpoint(ep string) bool {
// the incoming JSON response.
return false
}
ip := ipp.IP()
ip := ipp.IP
if ip.Zone() != "" {
return false
}

View File

@@ -1,98 +0,0 @@
# 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.

View File

@@ -1,7 +0,0 @@
// 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

View File

@@ -1,86 +0,0 @@
// 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
}

View File

@@ -1,30 +0,0 @@
// 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 }

View File

@@ -1,22 +0,0 @@
// 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)
}
}

View File

@@ -1,77 +0,0 @@
# 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;
};
};
}

View File

@@ -1,805 +0,0 @@
// 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 := &regexValue{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}}" ]
`

View File

@@ -52,7 +52,7 @@ func (s FirewallType) key(src, dst netaddr.IPPort) fwKey {
switch s {
case EndpointIndependentFirewall:
case AddressDependentFirewall:
k.dst = k.dst.WithIP(dst.IP())
k.dst.IP = dst.IP
case AddressAndPortDependentFirewall:
k.dst = dst
default:

View File

@@ -62,7 +62,7 @@ func (t NATType) key(src, dst netaddr.IPPort) natKey {
switch t {
case EndpointIndependentNAT:
case AddressDependentNAT:
k.dst = k.dst.WithIP(dst.IP())
k.dst.IP = dst.IP
case AddressAndPortDependentNAT:
k.dst = dst
default:
@@ -171,7 +171,7 @@ func (n *SNAT44) HandleIn(p *Packet, iif *Interface) *Packet {
func (n *SNAT44) HandleForward(p *Packet, iif, oif *Interface) *Packet {
switch {
case oif == n.ExternalInterface:
if p.Src.IP() == oif.V4() {
if p.Src.IP == oif.V4() {
// Packet already NATed and is just retraversing Forward,
// don't touch it again.
return p
@@ -237,7 +237,10 @@ func (n *SNAT44) allocateMappedPort() (net.PacketConn, netaddr.IPPort) {
if err != nil {
panic(fmt.Sprintf("ran out of NAT ports: %v", err))
}
addr := netaddr.IPPortFrom(ip, uint16(pc.LocalAddr().(*net.UDPAddr).Port))
addr := netaddr.IPPort{
IP: ip,
Port: uint16(pc.LocalAddr().(*net.UDPAddr).Port),
}
return pc, addr
}

View File

@@ -138,7 +138,7 @@ func (n *Network) allocIPv4(iface *Interface) netaddr.IP {
return netaddr.IP{}
}
if n.lastV4.IsZero() {
n.lastV4 = n.Prefix4.IP()
n.lastV4 = n.Prefix4.IP
}
a := n.lastV4.As16()
addOne(&a, 15)
@@ -157,7 +157,7 @@ func (n *Network) allocIPv6(iface *Interface) netaddr.IP {
return netaddr.IP{}
}
if n.lastV6.IsZero() {
n.lastV6 = n.Prefix6.IP()
n.lastV6 = n.Prefix6.IP
}
a := n.lastV6.As16()
addOne(&a, 15)
@@ -183,15 +183,15 @@ func (n *Network) write(p *Packet) (num int, err error) {
n.mu.Lock()
defer n.mu.Unlock()
iface, ok := n.machine[p.Dst.IP()]
iface, ok := n.machine[p.Dst.IP]
if !ok {
// If the destination is within the network's authoritative
// range, no route to host.
if p.Dst.IP().Is4() && n.Prefix4.Contains(p.Dst.IP()) {
if p.Dst.IP.Is4() && n.Prefix4.Contains(p.Dst.IP) {
p.Trace("no route to %v", p.Dst.IP)
return len(p.Payload), nil
}
if p.Dst.IP().Is6() && n.Prefix6.Contains(p.Dst.IP()) {
if p.Dst.IP.Is6() && n.Prefix6.Contains(p.Dst.IP) {
p.Trace("no route to %v", p.Dst.IP)
return len(p.Payload), nil
}
@@ -363,7 +363,7 @@ func (m *Machine) isLocalIP(ip netaddr.IP) bool {
func (m *Machine) deliverIncomingPacket(p *Packet, iface *Interface) {
p.setLocator("mach=%s if=%s", m.Name, iface.name)
if m.isLocalIP(p.Dst.IP()) {
if m.isLocalIP(p.Dst.IP) {
m.deliverLocalPacket(p, iface)
} else {
m.forwardPacket(p, iface)
@@ -391,13 +391,13 @@ func (m *Machine) deliverLocalPacket(p *Packet, iface *Interface) {
defer m.mu.Unlock()
conns := m.conns4
if p.Dst.IP().Is6() {
if p.Dst.IP.Is6() {
conns = m.conns6
}
possibleDsts := []netaddr.IPPort{
p.Dst,
netaddr.IPPortFrom(v6unspec, p.Dst.Port()),
netaddr.IPPortFrom(v4unspec, p.Dst.Port()),
netaddr.IPPort{IP: v6unspec, Port: p.Dst.Port},
netaddr.IPPort{IP: v4unspec, Port: p.Dst.Port},
}
for _, dest := range possibleDsts {
c, ok := conns[dest]
@@ -417,7 +417,7 @@ func (m *Machine) deliverLocalPacket(p *Packet, iface *Interface) {
}
func (m *Machine) forwardPacket(p *Packet, iif *Interface) {
oif, err := m.interfaceForIP(p.Dst.IP())
oif, err := m.interfaceForIP(p.Dst.IP)
if err != nil {
p.Trace("%v", err)
return
@@ -501,7 +501,7 @@ func (m *Machine) Attach(interfaceName string, n *Network) *Interface {
}
}
sort.Slice(m.routes, func(i, j int) bool {
return m.routes[i].prefix.Bits() > m.routes[j].prefix.Bits()
return m.routes[i].prefix.Bits > m.routes[j].prefix.Bits
})
return f
@@ -515,33 +515,33 @@ var (
func (m *Machine) writePacket(p *Packet) (n int, err error) {
p.setLocator("mach=%s", m.Name)
iface, err := m.interfaceForIP(p.Dst.IP())
iface, err := m.interfaceForIP(p.Dst.IP)
if err != nil {
p.Trace("%v", err)
return 0, err
}
origSrcIP := p.Src.IP()
origSrcIP := p.Src.IP
switch {
case p.Src.IP() == v4unspec:
case p.Src.IP == v4unspec:
p.Trace("assigning srcIP=%s", iface.V4())
p.Src = p.Src.WithIP(iface.V4())
case p.Src.IP() == v6unspec:
p.Src.IP = iface.V4()
case p.Src.IP == v6unspec:
// v6unspec in Go means "any src, but match address families"
if p.Dst.IP().Is6() {
if p.Dst.IP.Is6() {
p.Trace("assigning srcIP=%s", iface.V6())
p.Src = p.Src.WithIP(iface.V6())
} else if p.Dst.IP().Is4() {
p.Src.IP = iface.V6()
} else if p.Dst.IP.Is4() {
p.Trace("assigning srcIP=%s", iface.V4())
p.Src = p.Src.WithIP(iface.V4())
p.Src.IP = iface.V4()
}
default:
if !iface.Contains(p.Src.IP()) {
err := fmt.Errorf("can't send to %v with src %v on interface %v", p.Dst.IP(), p.Src.IP(), iface)
if !iface.Contains(p.Src.IP) {
err := fmt.Errorf("can't send to %v with src %v on interface %v", p.Dst.IP, p.Src.IP, iface)
p.Trace("%v", err)
return 0, err
}
}
if p.Src.IP().IsZero() {
if p.Src.IP.IsZero() {
err := fmt.Errorf("no matching address for address family for %v", origSrcIP)
p.Trace("%v", err)
return 0, err
@@ -602,12 +602,12 @@ func (m *Machine) pickEphemPort() (port uint16, err error) {
func (m *Machine) portInUseLocked(port uint16) bool {
for ipp := range m.conns4 {
if ipp.Port() == port {
if ipp.Port == port {
return true
}
}
for ipp := range m.conns6 {
if ipp.Port() == port {
if ipp.Port == port {
return true
}
}
@@ -617,7 +617,7 @@ func (m *Machine) portInUseLocked(port uint16) bool {
func (m *Machine) registerConn4(c *conn) error {
m.mu.Lock()
defer m.mu.Unlock()
if c.ipp.IP().Is6() && c.ipp.IP() != v6unspec {
if c.ipp.IP.Is6() && c.ipp.IP != v6unspec {
return fmt.Errorf("registerConn4 got IPv6 %s", c.ipp)
}
return registerConn(&m.conns4, c)
@@ -632,7 +632,7 @@ func (m *Machine) unregisterConn4(c *conn) {
func (m *Machine) registerConn6(c *conn) error {
m.mu.Lock()
defer m.mu.Unlock()
if c.ipp.IP().Is4() {
if c.ipp.IP.Is4() {
return fmt.Errorf("registerConn6 got IPv4 %s", c.ipp)
}
return registerConn(&m.conns6, c)
@@ -707,7 +707,7 @@ func (m *Machine) ListenPacket(ctx context.Context, network, address string) (ne
return nil, nil
}
}
ipp := netaddr.IPPortFrom(ip, port)
ipp := netaddr.IPPort{IP: ip, Port: port}
c := &conn{
m: m,

View File

@@ -49,8 +49,8 @@ func TestSendPacket(t *testing.T) {
ifFoo := foo.Attach("eth0", internet)
ifBar := bar.Attach("enp0s1", internet)
fooAddr := netaddr.IPPortFrom(ifFoo.V4(), 123)
barAddr := netaddr.IPPortFrom(ifBar.V4(), 456)
fooAddr := netaddr.IPPort{IP: ifFoo.V4(), Port: 123}
barAddr := netaddr.IPPort{IP: ifBar.V4(), Port: 456}
ctx := context.Background()
fooPC, err := foo.ListenPacket(ctx, "udp4", fooAddr.String())
@@ -111,10 +111,10 @@ func TestMultiNetwork(t *testing.T) {
t.Fatal(err)
}
clientAddr := netaddr.IPPortFrom(ifClient.V4(), 123)
natLANAddr := netaddr.IPPortFrom(ifNATLAN.V4(), 456)
natWANAddr := netaddr.IPPortFrom(ifNATWAN.V4(), 456)
serverAddr := netaddr.IPPortFrom(ifServer.V4(), 789)
clientAddr := netaddr.IPPort{IP: ifClient.V4(), Port: 123}
natLANAddr := netaddr.IPPort{IP: ifNATLAN.V4(), Port: 456}
natWANAddr := netaddr.IPPort{IP: ifNATWAN.V4(), Port: 456}
serverAddr := netaddr.IPPort{IP: ifServer.V4(), Port: 789}
const msg1, msg2 = "hello", "world"
if _, err := natPC.WriteTo([]byte(msg1), clientAddr.UDPAddr()); err != nil {
@@ -154,8 +154,8 @@ type trivialNAT struct {
}
func (n *trivialNAT) HandleIn(p *Packet, iface *Interface) *Packet {
if iface == n.wanIf && p.Dst.IP() == n.wanIf.V4() {
p.Dst = p.Dst.WithIP(n.clientIP)
if iface == n.wanIf && p.Dst.IP == n.wanIf.V4() {
p.Dst.IP = n.clientIP
}
return p
}
@@ -167,13 +167,13 @@ func (n trivialNAT) HandleOut(p *Packet, iface *Interface) *Packet {
func (n *trivialNAT) HandleForward(p *Packet, iif, oif *Interface) *Packet {
// Outbound from LAN -> apply NAT, continue
if iif == n.lanIf && oif == n.wanIf {
if p.Src.IP() == n.clientIP {
p.Src = p.Src.WithIP(n.wanIf.V4())
if p.Src.IP == n.clientIP {
p.Src.IP = n.wanIf.V4()
}
return p
}
// Return traffic to LAN, allow if right dst.
if iif == n.wanIf && oif == n.lanIf && p.Dst.IP() == n.clientIP {
if iif == n.wanIf && oif == n.lanIf && p.Dst.IP == n.clientIP {
return p
}
// Else drop.
@@ -216,7 +216,7 @@ func TestPacketHandler(t *testing.T) {
}
const msg = "some message"
serverAddr := netaddr.IPPortFrom(ifServer.V4(), 456)
serverAddr := netaddr.IPPort{IP: ifServer.V4(), Port: 456}
if _, err := clientPC.WriteTo([]byte(msg), serverAddr.UDPAddr()); err != nil {
t.Fatal(err)
}
@@ -230,7 +230,7 @@ func TestPacketHandler(t *testing.T) {
if string(buf) != msg {
t.Errorf("read %q; want %q", buf, msg)
}
mappedAddr := netaddr.IPPortFrom(ifNATWAN.V4(), 123)
mappedAddr := netaddr.IPPort{IP: ifNATWAN.V4(), Port: 123}
if addr.String() != mappedAddr.String() {
t.Errorf("addr = %q; want %q", addr, mappedAddr)
}

View File

@@ -250,7 +250,7 @@ func TestConciseDiffFrom(t *testing.T) {
DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
DiscoKey: testDiscoKey("f00f00f00f"),
AllowedIPs: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
},
},
},
@@ -263,7 +263,7 @@ func TestConciseDiffFrom(t *testing.T) {
DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
DiscoKey: testDiscoKey("ba4ba4ba4b"),
AllowedIPs: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
},
},
},

View File

@@ -0,0 +1,74 @@
// 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)
}

View File

@@ -0,0 +1,52 @@
// 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)
}
}

View File

@@ -10,6 +10,7 @@
package wgkey
import (
"bytes"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
@@ -71,11 +72,10 @@ 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) AppendTo(b []byte) []byte { return appendKey(b, "", k) }
func (k Key) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
func (k Key) String() string { return k.ShortString() }
func (k Key) HexString() string { return hex.EncodeToString(k[:]) }
func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
func (k *Key) ShortString() string {
// The goal here is to generate "[" + base64.StdEncoding.EncodeToString(k[:])[:5] + "]".
@@ -178,17 +178,13 @@ func (k *Private) Public() Key {
return (Key)(p)
}
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) {
// TODO(josharian): use encoding/hex instead?
buf := new(bytes.Buffer)
fmt.Fprintf(buf, `privkey:%x`, k[:])
return buf.Bytes(), nil
}
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:`) {

View File

@@ -21,48 +21,46 @@ 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:]
}
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 {
if len(s) > maxNameLength {
return "", fmt.Errorf("%q is too long to be a DNS name", s)
}
st := 0
for i := 0; i < len(s); i++ {
if s[i] != '.' {
continue
fs := strings.Split(s, ".")
for _, f := range fs {
if !validLabel(f) {
return "", fmt.Errorf("%q is not a valid DNS label", f)
}
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
}
if raw[len(raw)-1] != '.' {
raw = raw + "."
return FQDN(s + "."), nil
}
func validLabel(s string) bool {
if len(s) == 0 || len(s) > maxLabelLength {
return false
}
return FQDN(raw), nil
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
}
// WithTrailingDot returns f as a string, with a trailing dot.
@@ -94,6 +92,51 @@ 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 {

View File

@@ -24,7 +24,6 @@ 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},
@@ -185,24 +184,3 @@ 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)
}
})
}
}

9
version/GENERATE.go Normal file
View File

@@ -0,0 +1,9 @@
// 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

2
version/all.do Normal file
View File

@@ -0,0 +1,2 @@
redo-ifchange ver.go version.xcconfig version.h

1
version/clean.do Normal file
View File

@@ -0,0 +1 @@
rm -f *~ .*~ describe.txt long.txt short.txt version.xcconfig ver.go version.h version version-info.sh

102
version/mkversion_test.go Normal file
View File

@@ -0,0 +1,102 @@
// 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)
}
}
}

9
version/ver.go.do Normal file
View File

@@ -0,0 +1,9 @@
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

14
version/ver.go.in Normal file
View File

@@ -0,0 +1,14 @@
// 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}"

Some files were not shown because too many files have changed in this diff Show More