Compare commits
43 Commits
simenghe/f
...
simeng-pin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11a1a9096d | ||
|
|
97967c0a85 | ||
|
|
8875967f2b | ||
|
|
63df9e2a4e | ||
|
|
bb3db20e74 | ||
|
|
1bc78312c3 | ||
|
|
ba4ee26313 | ||
|
|
4005427078 | ||
|
|
4d00c7ef7e | ||
|
|
5d1be01d44 | ||
|
|
6725596a0a | ||
|
|
f925d09b83 | ||
|
|
8cf74ee5e3 | ||
|
|
a374db1f8b | ||
|
|
866c2827d6 | ||
|
|
93b5da680c | ||
|
|
e5c813963b | ||
|
|
da8b9adc51 | ||
|
|
16b8233459 | ||
|
|
88487ee067 | ||
|
|
be14720df4 | ||
|
|
27773080c1 | ||
|
|
21dd79ab91 | ||
|
|
744300ee96 | ||
|
|
7de0421f17 | ||
|
|
0438c0deda | ||
|
|
01be3630b9 | ||
|
|
d52cf5e99b | ||
|
|
a14bf1fdc2 | ||
|
|
65879efae4 | ||
|
|
031a6fe1db | ||
|
|
940e1c7690 | ||
|
|
49e733773c | ||
|
|
c56829eff7 | ||
|
|
73bb80e42b | ||
|
|
c8fabf4f41 | ||
|
|
2455f1035c | ||
|
|
c0f692b725 | ||
|
|
54ac8e8022 | ||
|
|
84d5c95f65 | ||
|
|
f894fad4f7 | ||
|
|
b40a69e846 | ||
|
|
e1b16b6b52 |
36
.github/workflows/xe-experimental-vm-test.yml
vendored
36
.github/workflows/xe-experimental-vm-test.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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}" "$@"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -64,6 +64,7 @@ var pingArgs struct {
|
||||
}
|
||||
|
||||
func runPing(ctx context.Context, args []string) error {
|
||||
fmt.Println("runPing")
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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
|
||||
|
||||
@@ -17,10 +17,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/klauspost/compress/huff0 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
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
@@ -180,7 +179,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled+
|
||||
|
||||
@@ -7,6 +7,7 @@ package controlclient
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -157,6 +158,7 @@ func (c *Auto) Start() {
|
||||
//
|
||||
// It should be called whenever there's something new to tell the server.
|
||||
func (c *Auto) sendNewMapRequest() {
|
||||
log.Println("sendNewMapRequest breakpoint")
|
||||
c.mu.Lock()
|
||||
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
@@ -576,12 +578,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 +591,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 +714,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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -81,6 +81,11 @@ type Direct struct {
|
||||
everEndpoints bool // whether we've ever had non-empty endpoints
|
||||
localPort uint16 // or zero to mean auto
|
||||
}
|
||||
type Pinger interface {
|
||||
// Ping is a request to start a discovery 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 Options struct {
|
||||
Persist persist.Persist // initial persistent data
|
||||
@@ -96,6 +101,7 @@ type Options struct {
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
Pinger Pinger
|
||||
|
||||
// KeepSharerAndUserSplit controls whether the client
|
||||
// understands Node.Sharer. If false, the Sharer is mapped to the User.
|
||||
@@ -105,18 +111,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 {
|
||||
@@ -568,6 +562,7 @@ func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
// maxPolls is how many network maps to download; common values are 1
|
||||
// or -1 (to keep a long-poll query open to the server).
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
log.Println("POLLNETMAP BREAKPOINT")
|
||||
return c.sendMapRequest(ctx, maxPolls, cb)
|
||||
}
|
||||
|
||||
@@ -575,6 +570,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.N
|
||||
// but does not fetch anything. It returns an error if the server did not return a
|
||||
// successful 200 OK response.
|
||||
func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
|
||||
log.Println("SendLiteMapUpdate BREAKPOINT")
|
||||
return c.sendMapRequest(ctx, 1, nil)
|
||||
}
|
||||
|
||||
@@ -586,6 +582,7 @@ const pollTimeout = 120 * time.Second
|
||||
// cb nil means to omit peers.
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
c.mu.Lock()
|
||||
log.Println("sendMapRequest ENDPOINT")
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
serverKey := c.serverKey
|
||||
@@ -776,6 +773,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
|
||||
if pr := resp.PingRequest; pr != nil {
|
||||
// return err
|
||||
log.Println("Ping Triggered")
|
||||
for i := 0; i < 10; i++ {
|
||||
c.CustomPing(&resp)
|
||||
}
|
||||
go answerPing(c.logf, c.httpc, pr)
|
||||
}
|
||||
|
||||
@@ -874,6 +876,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
c.expiry = &nm.Expiry
|
||||
c.mu.Unlock()
|
||||
|
||||
// log.Println("MAPRESPONSE: ", resp.Node)
|
||||
// c.logf("MAPRESPONSE: %v", resp.Node)
|
||||
cb(nm)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
@@ -1227,49 +1231,34 @@ 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")
|
||||
// Run the ping suite from this client to another one
|
||||
// Send the ping results via http to the adminhttp handlers.
|
||||
// This is where we hopefully will run the ping suite similar to CLI
|
||||
func (c *Direct) CustomPing(mr *tailcfg.MapResponse) bool {
|
||||
log.Printf("Custom Ping Triggered with %d number of peers\n", len(mr.Peers))
|
||||
log.Println("Ping Request: ", mr.PingRequest)
|
||||
log.Println("ALOHA")
|
||||
log.Println("CP PEERLIST : ", mr.Peers, mr.PeersChanged, mr.PeersRemoved, mr.PeerSeenChange)
|
||||
if len(mr.Peers) > 0 {
|
||||
log.Println("Peer data: ", mr.Peers[0].ID)
|
||||
}
|
||||
machinePrivKey, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getMachinePrivKey: %w", err)
|
||||
}
|
||||
if machinePrivKey.IsZero() {
|
||||
return errors.New("getMachinePrivKey returned zero key")
|
||||
ip := mr.PingRequest.TestIP
|
||||
log.Println("TestIP : ", ip)
|
||||
start := time.Now()
|
||||
// Run the ping
|
||||
var pingRes *ipnstate.PingResult
|
||||
for i := 1; i <= 10; i++ {
|
||||
log.Println("Ping attempt ", i)
|
||||
go c.pinger.Ping(ip, true, func(res *ipnstate.PingResult) {
|
||||
log.Println("Callback", res, (res.NodeIP))
|
||||
pingRes = res
|
||||
})
|
||||
}
|
||||
|
||||
bodyData, err := encode(req, &serverKey, &machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := bytes.NewReader(bodyData)
|
||||
log.Println("PINGRES", pingRes)
|
||||
duration := time.Since(start)
|
||||
// Send the data to the handler in api.go admin/api/ping
|
||||
log.Printf("Ping operation took %f seconds\n", duration.Seconds())
|
||||
|
||||
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
|
||||
return len(mr.Peers) > 0
|
||||
}
|
||||
|
||||
@@ -103,3 +103,37 @@ func TestNewHostinfo(t *testing.T) {
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
// Currently not working properly
|
||||
func TestPingFromMapResponse(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
opts := Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (wgkey.Private, error) {
|
||||
return key, nil
|
||||
},
|
||||
}
|
||||
c, err := NewDirect(opts)
|
||||
if c == nil || err != nil {
|
||||
t.Errorf("Direct not created %w", err)
|
||||
}
|
||||
peers := []*tailcfg.Node{
|
||||
{ID: 1},
|
||||
{ID: 2},
|
||||
{ID: 3},
|
||||
}
|
||||
pingRequest := tailcfg.PingRequest{URL: "localhost:3040", Log: true, PayloadSize: 10}
|
||||
mr := &tailcfg.MapResponse{Peers: peers, Domain: "DumbTest", PingRequest: &pingRequest}
|
||||
if !c.CustomPing(mr) {
|
||||
t.Errorf("Custom ping failed!\n")
|
||||
}
|
||||
t.Log("Successful ping")
|
||||
}
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
15
go.mod
15
go.mod
@@ -5,28 +5,25 @@ 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/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
|
||||
github.com/gliderlabs/ssh v0.3.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/go-cmp v0.5.5
|
||||
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/jsimonetti/rtnetlink v0.0.0-20210409061457-9561dc9288a7
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.12.2
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mdlayher/netlink v1.4.1
|
||||
github.com/mdlayher/netlink v1.4.0
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
|
||||
github.com/miekg/dns v1.1.42
|
||||
github.com/pborman/getopt v1.1.0
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/pkg/sftp v1.13.0
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
@@ -34,14 +31,14 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
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
|
||||
inet.af/netaddr v0.0.0-20210523191804-d57edf19c517
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
|
||||
51
go.sum
51
go.sum
@@ -55,8 +55,6 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aws/aws-sdk-go v1.38.52 h1:7NKcUyTG/CyDX835kq04DDNe8vXaJhbGW8ThemHb18A=
|
||||
github.com/aws/aws-sdk-go v1.38.52/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
@@ -233,9 +231,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f h1:7MmqygqdeJtziBUpm4Z9ThROFZUaVGaePMfcDnluf1E=
|
||||
github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f/go.mod h1:n1ej5+FqyEytMt/mugVDZLIiqTMO+vsrgY+kM6ohzN0=
|
||||
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
|
||||
@@ -311,10 +308,6 @@ github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:x
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48=
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
@@ -327,8 +320,8 @@ github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR7
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190 h1:iycCSDo8EKVueI9sfVBBJmtNn9DnXV/K1YWwEJO+uOs=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210409061457-9561dc9288a7 h1:0pS4NUf9WPvydLWHx2VHafjEyfN8vQrAxl/n3Kt2K9c=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210409061457-9561dc9288a7/go.mod h1:+fPVEwpdpYDhPa086y6yIAwUno3cBJZw15Fds43LDRA=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
@@ -348,7 +341,6 @@ github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+O
|
||||
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -366,7 +358,11 @@ github.com/kyoh86/exportloopref v0.1.8 h1:5Ry/at+eFdkX9Vsdw3qU4YkvGtzuVfzT4X7S77
|
||||
github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY=
|
||||
@@ -402,13 +398,10 @@ github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klX
|
||||
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
|
||||
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
|
||||
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI=
|
||||
github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697 h1:PBb7ld5cQGfxHF2pKvb/ydtuPwdRaltGI4e0QSCuiNI=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
|
||||
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 h1:qEtkL8n1DAHpi5/AOgAckwGQUlMe4+jhL/GMt+GKIks=
|
||||
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY=
|
||||
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
@@ -477,8 +470,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pkg/sftp v1.13.0 h1:Riw6pgOKK41foc1I1Uu03CjvbLZDXeGpInycM4shXoI=
|
||||
github.com/pkg/sftp v1.13.0/go.mod h1:41g+FIPlQUTDCveupEmEA65IoiQFrtgCeDopC4ajGIM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polyfloyd/go-errorlint v0.0.0-20201006195004-351e25ade6e3/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
|
||||
@@ -579,6 +570,8 @@ github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3 h1:fEubocuQkrl
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210522003738-46b531feb08a h1:ujoIjR8p8HEVy26RnOe6U5aJwaMYFrIa4cpGGeZF5oc=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210522003738-46b531feb08a/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
@@ -647,8 +640,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
@@ -716,6 +709,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210521195947-fe42d452be8f h1:Si4U+UcgJzya9kpiEUJKQvjr512OLli+gL4poHrz93U=
|
||||
golang.org/x/net v0.0.0-20210521195947-fe42d452be8f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -770,7 +765,9 @@ golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -789,9 +786,10 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1 h1:lCnv+lfrU9FRPGf8NeRuWAAPjNnema5WtBinMgs1fD8=
|
||||
golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8=
|
||||
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
@@ -801,6 +799,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7-0.20210524175448-3115f89c4b99 h1:ZEXtoJu1S0ie/EmdYnjY3CqaCCZxnldL+K1ftMITD2Q=
|
||||
golang.org/x/text v0.3.7-0.20210524175448-3115f89c4b99/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -863,6 +862,8 @@ golang.org/x/tools v0.0.0-20201121010211-780cb80bd7fb/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -873,6 +874,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210521230051-c27ff9b9f6f7/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5 h1:5D3v3AKu7ktIhDlqZhZ4+YeNKsW+dnc2+zfFAdhwa8M=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.15-0.20210525143335-94c0476d63e3 h1:Xw0ZuZcvq981iPGZoLrUXhrK2jOJAw/B6gZxc6g8FsU=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.15-0.20210525143335-94c0476d63e3/go.mod h1:f/UVhQ6vXZKDodGB3Glgwu9B3djRxR14jIbcuxD8NBw=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -913,6 +918,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -950,8 +957,8 @@ honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzE
|
||||
honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ=
|
||||
honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3 h1:RlarOdsmOUCCvy7Xm1JchJIGuQsuKwD/Lo1bjYmfuQI=
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210523191804-d57edf19c517 h1:gieHAlViNfjNt0m6gKr4aazCMXQobPMOqeyQ1ZN5ekw=
|
||||
inet.af/netaddr v0.0.0-20210523191804-d57edf19c517/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 h1:DNtszwGa6w76qlIr+PbPEnlBJdiRV8SaxeigOy0q1gg=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22/go.mod h1:GVx+5OZtbG4TVOW5ilmyRZAZXr1cNwfqUEkTOtWK0PM=
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763 h1:gPSJmmVzmdy4kHhlCMx912GdiUz3k/RzJGg0ADqy1dg=
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -294,6 +295,7 @@ func (b *LocalBackend) Prefs() *ipn.Prefs {
|
||||
// Status returns the latest status of the backend and its
|
||||
// sub-components.
|
||||
func (b *LocalBackend) Status() *ipnstate.Status {
|
||||
log.Println("Status ENDPOINT")
|
||||
sb := new(ipnstate.StatusBuilder)
|
||||
b.UpdateStatus(sb)
|
||||
return sb.Status()
|
||||
@@ -453,13 +455,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
|
||||
@@ -657,12 +652,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 +705,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
|
||||
@@ -837,6 +825,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
DiscoPublicKey: discoPublic,
|
||||
DebugFlags: debugFlags,
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
Pinger: b.e,
|
||||
|
||||
// Don't warn about broken Linux IP forwading when
|
||||
// netstack is being used.
|
||||
@@ -929,8 +918,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 {
|
||||
@@ -987,8 +976,7 @@ func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
|
||||
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 +1007,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
|
||||
@@ -1888,15 +1876,6 @@ 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 {
|
||||
@@ -2350,6 +2329,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 +2356,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
|
||||
}
|
||||
@@ -2577,42 +2561,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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/flowtrack"
|
||||
@@ -232,6 +233,7 @@ type TSMPPongReply struct {
|
||||
// AsTSMPPong returns pp as a TSMPPongReply and whether it is one.
|
||||
// The pong.IPHeader field is not populated.
|
||||
func (pp *Parsed) AsTSMPPong() (pong TSMPPongReply, ok bool) {
|
||||
log.Println("TSMPPONG")
|
||||
if pp.IPProto != ipproto.TSMP {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
@@ -19,26 +18,20 @@ import (
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package tstun
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -276,6 +277,7 @@ func (t *Wrapper) poll() {
|
||||
var magicDNSIPPort = netaddr.MustParseIPPort("100.100.100.100:0")
|
||||
|
||||
func (t *Wrapper) filterOut(p *packet.Parsed) filter.Response {
|
||||
log.Println("FILTEROUT")
|
||||
// Fake ICMP echo responses to MagicDNS (100.100.100.100).
|
||||
if p.IsEchoRequest() && p.Dst == magicDNSIPPort {
|
||||
header := p.ICMP4Header()
|
||||
@@ -376,6 +378,7 @@ func (t *Wrapper) Read(buf []byte, offset int) (int, error) {
|
||||
}
|
||||
|
||||
func (t *Wrapper) filterIn(buf []byte) filter.Response {
|
||||
log.Println("FILTERIN")
|
||||
p := parsedPacketPool.Get().(*packet.Parsed)
|
||||
defer parsedPacketPool.Put(p)
|
||||
p.Decode(buf)
|
||||
@@ -532,6 +535,7 @@ func (t *Wrapper) InjectInboundCopy(packet []byte) error {
|
||||
}
|
||||
|
||||
func (t *Wrapper) injectOutboundPong(pp *packet.Parsed, req packet.TSMPPingRequest) {
|
||||
log.Println("INJECT OUTBOUND")
|
||||
pong := packet.TSMPPongReply{
|
||||
Data: req.Data,
|
||||
}
|
||||
@@ -568,8 +572,10 @@ func (t *Wrapper) InjectOutbound(packet []byte) error {
|
||||
}
|
||||
select {
|
||||
case <-t.closed:
|
||||
log.Println("Closed")
|
||||
return ErrClosed
|
||||
case t.outbound <- packet:
|
||||
log.Println("t.outbound <- packet")
|
||||
return 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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// AppSharedDir is a string set by the iOS or Android app on start
|
||||
@@ -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() {
|
||||
|
||||
@@ -749,6 +749,9 @@ type MapRequest struct {
|
||||
// * "minimize-netmap": have control minimize the netmap, removing
|
||||
// peers that are unreachable per ACLS.
|
||||
DebugFlags []string `json:",omitempty"`
|
||||
|
||||
// Basic boolean field to determine if a Ping is being intitiated
|
||||
Ping bool
|
||||
}
|
||||
|
||||
// PortRange represents a range of UDP or TCP port numbers.
|
||||
@@ -884,6 +887,27 @@ type PingRequest struct {
|
||||
// Log is whether to log about this ping in the success case.
|
||||
// For failure cases, the client will log regardless.
|
||||
Log bool `json:",omitempty"`
|
||||
|
||||
Initiator string // admin@email; "system" (for Tailscale)
|
||||
TestIP netaddr.IP
|
||||
Types string // empty means all: TSMP+ICMP+disco
|
||||
StopAfterNDirect int // 1 means stop on 1st direct ping; 4 means 4 direct pings; 0 means do MaxPings and stop
|
||||
MaxPings int // MaxPings total, direct or DERPed
|
||||
PayloadSize int // default: 0 extra bytes
|
||||
}
|
||||
|
||||
// According to https://roamresearch.com/#/app/ts-corp/page/4Bn_Famn2
|
||||
// Client can stream responses back via HTTP
|
||||
// We will add a struct with the proper fields
|
||||
type StreamedPingResult struct {
|
||||
IP netaddr.IP
|
||||
SeqNum int // somewhat redundant with TxID but for clarity
|
||||
SentTo NodeID // for exit/subnet relays
|
||||
TxID string // N hex bytes random
|
||||
Dir string // "in"/"out"
|
||||
Type string // ICMP, disco, TSMP, ...
|
||||
Via string // "direct", "derp-nyc", ...
|
||||
Seconds float64 // for Dir "in" only
|
||||
}
|
||||
|
||||
type MapResponse struct {
|
||||
@@ -1174,31 +1198,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
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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 (
|
||||
@@ -10,7 +11,6 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -42,9 +43,10 @@ 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")
|
||||
// var verbose = flag.Bool("verbose", true, "verbose debug logs")
|
||||
|
||||
var mainError atomic.Value // of error
|
||||
|
||||
@@ -57,12 +59,13 @@ func TestMain(m *testing.M) {
|
||||
fmt.Fprintf(os.Stderr, "FAIL: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -195,7 +198,7 @@ func TestTwoNodes(t *testing.T) {
|
||||
|
||||
func TestNodeAddressIPFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
@@ -224,57 +227,23 @@ func TestNodeAddressIPFields(t *testing.T) {
|
||||
d1.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
func TestAddPingRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(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
|
||||
}
|
||||
|
||||
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 +251,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 +269,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")
|
||||
}
|
||||
@@ -310,6 +279,9 @@ func newTestEnv(t testing.TB, bins *Binaries) *testEnv {
|
||||
DERPMap: derpMap,
|
||||
}
|
||||
trafficTrap := new(trafficTrap)
|
||||
log.Println("SERVER ATTACHED")
|
||||
log.Println(len(control.PingRequestC))
|
||||
// go func() { control.PingRequestC <- true }()
|
||||
e := &testEnv{
|
||||
t: t,
|
||||
Binaries: bins,
|
||||
@@ -383,7 +355,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 +433,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 +460,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
|
||||
@@ -558,7 +587,7 @@ func (lc *logCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
for _, ent := range jreq {
|
||||
fmt.Fprintf(&lc.buf, "%s\n", strings.TrimSpace(ent.Text))
|
||||
if *verbose {
|
||||
if testing.Verbose() {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSpace(ent.Text))
|
||||
}
|
||||
}
|
||||
@@ -657,3 +686,132 @@ func (w *authURLParserWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
type panicOnUseTransport struct{}
|
||||
|
||||
func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
panic("unexpected HTTP request")
|
||||
}
|
||||
func TestTwoNodePing(t *testing.T) {
|
||||
|
||||
// < --->
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
t.Log("Env :", env.ControlServer.URL)
|
||||
res, err := http.Get(env.ControlServer.URL + "/ping")
|
||||
t.Log("RESPONSE", res)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer env.Close()
|
||||
|
||||
// Create two nodes:
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n2 := newTestNode(t, env)
|
||||
d2 := n2.StartDaemon(t)
|
||||
defer d2.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n2.AwaitListening(t)
|
||||
n1.MustUp()
|
||||
n2.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
n2.AwaitRunning(t)
|
||||
ip1 := n1.AwaitIP(t)
|
||||
ip2 := n2.AwaitIP(t)
|
||||
t.Logf("Node IPs : %s, %s\n", ip1, ip2)
|
||||
|
||||
if err := tstest.WaitFor(2*time.Second, func() error {
|
||||
st := n1.MustStatus(t)
|
||||
t.Log("CURPEER", len(st.Peer))
|
||||
var peers []*ipnstate.PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
jsonForm, _ := json.MarshalIndent(peers[0], "", " ")
|
||||
t.Log("PeerStatus", string(jsonForm))
|
||||
if len(st.Peer) == 0 {
|
||||
return errors.New("no peers")
|
||||
}
|
||||
if len(st.Peer) > 1 {
|
||||
return fmt.Errorf("got %d peers; want 1", len(st.Peer))
|
||||
}
|
||||
peer := st.Peer[st.Peers()[0]]
|
||||
if peer.ID == st.Self.ID {
|
||||
return errors.New("peer is self")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
d2.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
// Tests if our addPingRequest function works
|
||||
func TestAddPingRequest(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
// Test such that we can simulate the ping instead of hardcoding in the map response
|
||||
func TestControlSelectivePing(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
log.Println("POSTSTARTUP")
|
||||
defer env.Close()
|
||||
|
||||
// Create two nodes:
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n2 := newTestNode(t, env)
|
||||
d2 := n2.StartDaemon(t)
|
||||
defer d2.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n2.AwaitListening(t)
|
||||
n1.MustUp()
|
||||
n2.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
n2.AwaitRunning(t)
|
||||
|
||||
// Wait for server to start serveMap
|
||||
if err := tstest.WaitFor(2*time.Second, func() error {
|
||||
t.Log("ENOUGHTIME")
|
||||
env.Control.AddControlPingRequest()
|
||||
if len(env.Control.PingRequestC) == 0 {
|
||||
return errors.New("failed to add to PingRequestC")
|
||||
}
|
||||
log.Println("CHANNEL LENGTH", len(env.Control.PingRequestC))
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Wait for a MapResponse
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
// Simulate the time needed for MapResponse method call.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if len(env.Control.PingRequestC) == 1 {
|
||||
t.Error("Expected PingRequestC to be empty")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
d1.MustCleanShutdown(t)
|
||||
d2.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
@@ -36,14 +36,16 @@ import (
|
||||
// Server is a control plane server. Its zero value is ready for use.
|
||||
// Everything is stored in-memory in one tailnet.
|
||||
type Server struct {
|
||||
Logf logger.Logf // nil means to use the log package
|
||||
DERPMap *tailcfg.DERPMap // nil means to use prod DERP map
|
||||
RequireAuth bool
|
||||
BaseURL string // must be set to e.g. "http://127.0.0.1:1234" with no trailing URL
|
||||
Verbose bool
|
||||
Logf logger.Logf // nil means to use the log package
|
||||
DERPMap *tailcfg.DERPMap // nil means to use prod DERP map
|
||||
RequireAuth bool
|
||||
BaseURL string // must be set to e.g. "http://127.0.0.1:1234" with no trailing URL
|
||||
Verbose bool
|
||||
PingRequestC chan bool
|
||||
|
||||
initMuxOnce sync.Once
|
||||
mux *http.ServeMux
|
||||
initMuxOnce sync.Once
|
||||
mux *http.ServeMux
|
||||
initPRchannelOnce sync.Once
|
||||
|
||||
mu sync.Mutex
|
||||
pubKey wgkey.Key
|
||||
@@ -54,7 +56,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.
|
||||
@@ -68,27 +69,6 @@ func (s *Server) NumNodes() int {
|
||||
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 {
|
||||
nodeKey tailcfg.NodeKey
|
||||
|
||||
@@ -117,14 +97,26 @@ func (s *Server) logf(format string, a ...interface{}) {
|
||||
}
|
||||
|
||||
func (s *Server) initMux() {
|
||||
log.Println("Mux inited")
|
||||
s.mux = http.NewServeMux()
|
||||
s.mux.HandleFunc("/", s.serveUnhandled)
|
||||
s.mux.HandleFunc("/key", s.serveKey)
|
||||
s.mux.HandleFunc("/machine/", s.serveMachine)
|
||||
s.mux.HandleFunc("/ping", s.receivePingInfo)
|
||||
s.mux.HandleFunc("/mockpingrequest", s.serveMockPing)
|
||||
}
|
||||
|
||||
func (s *Server) initPingRequestC() {
|
||||
log.Println("Channel created")
|
||||
s.PingRequestC = make(chan bool, 1)
|
||||
// s.AddControlPingRequest()
|
||||
// log.Println("Channel length : ", len(s.PingRequestC))
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("HTTPSERVE")
|
||||
s.initMuxOnce.Do(s.initMux)
|
||||
s.initPRchannelOnce.Do(s.initPingRequestC)
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -134,6 +126,12 @@ func (s *Server) serveUnhandled(w http.ResponseWriter, r *http.Request) {
|
||||
go panic(fmt.Sprintf("testcontrol.Server received unhandled request: %s", got.Bytes()))
|
||||
}
|
||||
|
||||
func (s *Server) serveMockPing(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
s.AddControlPingRequest()
|
||||
io.WriteString(w, "A ControlPingRequest has been queued for our next MapResponse.")
|
||||
}
|
||||
|
||||
func (s *Server) publicKey() wgkey.Key {
|
||||
pub, _ := s.keyPair()
|
||||
return pub
|
||||
@@ -198,13 +196,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()
|
||||
}
|
||||
|
||||
@@ -220,6 +211,16 @@ func (s *Server) AllNodes() (nodes []*tailcfg.Node) {
|
||||
return nodes
|
||||
}
|
||||
|
||||
// AddControlPingRequest enqueues a bool to PingRequestC.
|
||||
// in serveMap this will result to a ControlPingRequest
|
||||
// added to the next MapResponse sent to the client
|
||||
func (s *Server) AddControlPingRequest() {
|
||||
// Redundant check to avoid errors when called multiple times
|
||||
if len(s.PingRequestC) == 0 {
|
||||
s.PingRequestC <- true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getUser(nodeKey tailcfg.NodeKey) (*tailcfg.User, *tailcfg.Login) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -307,6 +308,7 @@ func (s *Server) CompleteAuth(authPathOrURL string) bool {
|
||||
}
|
||||
|
||||
func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tailcfg.MachineKey) {
|
||||
log.Println("SERVE REGISTER CALLED")
|
||||
var req tailcfg.RegisterRequest
|
||||
if err := s.decode(mkey, r.Body, &req); err != nil {
|
||||
panic(fmt.Sprintf("serveRegister: decode: %v", err))
|
||||
@@ -402,9 +404,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) {
|
||||
@@ -413,6 +412,24 @@ func (s *Server) updateLocked(source string, peers []tailcfg.NodeID) {
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a PingRequest to a MapResponse, we will ping the first peer.
|
||||
func (s *Server) addPingRequest(res *tailcfg.MapResponse) error {
|
||||
if len(res.Peers) == 0 {
|
||||
return errors.New("MapResponse has no peers to ping")
|
||||
}
|
||||
|
||||
if len(res.Peers[0].Addresses) == 0 || len(res.Peers[0].AllowedIPs) == 0 {
|
||||
return errors.New("peer has no Addresses or no AllowedIPs")
|
||||
}
|
||||
targetIP := res.Peers[0].AllowedIPs[0].IP()
|
||||
res.PingRequest = &tailcfg.PingRequest{URL: s.BaseURL + "/ping", TestIP: targetIP, Types: "tsmp"}
|
||||
// jsonRes, _ := json.MarshalIndent(res, "", " ")
|
||||
// log.Println("jsonprint", string(jsonRes))
|
||||
// log.Println("respeers", res.Peers)
|
||||
// log.Println("allnodes", s.AllNodes(), res.Node.AllowedIPs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendUpdate sends updateType to dst if dst is non-nil and
|
||||
// has capacity.
|
||||
func sendUpdate(dst chan<- updateType, updateType updateType) {
|
||||
@@ -444,6 +461,7 @@ func (s *Server) UpdateNode(n *tailcfg.Node) (peersToUpdate []tailcfg.NodeID) {
|
||||
}
|
||||
|
||||
func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey tailcfg.MachineKey) {
|
||||
log.Println("SERVEMAP CALLED")
|
||||
ctx := r.Context()
|
||||
|
||||
req := new(tailcfg.MapRequest)
|
||||
@@ -496,9 +514,20 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey tailcfg.M
|
||||
streaming := req.Stream && !req.ReadOnly
|
||||
compress := req.Compress != ""
|
||||
|
||||
log.Println("CREATED MAPREQ", *req)
|
||||
log.Println("REQUEST", r)
|
||||
log.Println("REQBODY", r.Body)
|
||||
w.WriteHeader(200)
|
||||
for {
|
||||
res, err := s.MapResponse(req)
|
||||
log.Println("LENGTHER", len(s.PingRequestC))
|
||||
select {
|
||||
case <-s.PingRequestC:
|
||||
log.Println("PINGADD", len(s.PingRequestC))
|
||||
s.addPingRequest(res)
|
||||
default:
|
||||
log.Println("NOTEXIST")
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: log
|
||||
return
|
||||
@@ -559,6 +588,7 @@ var prodDERPMap = derpmap.Prod()
|
||||
//
|
||||
// No updates to s are done here.
|
||||
func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, err error) {
|
||||
log.Println("MAPREQUEST : ", string(JsonPrint(req)))
|
||||
node := s.Node(req.NodeKey)
|
||||
if node == nil {
|
||||
// node key rotated away (once test server supports that)
|
||||
@@ -587,13 +617,6 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -744,3 +767,24 @@ func breakSameNodeMapResponseStreams(req *tailcfg.MapRequest) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// This is where the PUT requests will go
|
||||
func (s *Server) receivePingInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PUT" {
|
||||
log.Println("Received NON PUT request, should panic if this happens after")
|
||||
// panic("Only PUT requests are supported currently")
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
reqBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
panic("Failed to read request body")
|
||||
}
|
||||
log.Println("Ping Info Received", string(reqBody))
|
||||
w.WriteHeader(200)
|
||||
io.WriteString(w, "Ping Streamed Back : "+string(reqBody))
|
||||
}
|
||||
|
||||
func JsonPrint(item interface{}) []byte {
|
||||
res, _ := json.MarshalIndent(item, "", " ")
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -7,14 +7,11 @@
|
||||
package vms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -22,7 +19,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -30,36 +26,13 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
expect "github.com/google/goexpect"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
)
|
||||
|
||||
const (
|
||||
securePassword = "hunter2"
|
||||
bucketName = "tailscale-integration-vm-images"
|
||||
)
|
||||
|
||||
var (
|
||||
runVMTests = flag.Bool("run-vm-tests", false, "if set, run expensive VM based integration tests")
|
||||
noS3 = flag.Bool("no-s3", false, "if set, always download images from the public internet (risks breaking)")
|
||||
vmRamLimit = flag.Int("ram-limit", 4096, "the maximum number of megabytes of ram that can be used for VMs, must be greater than or equal to 1024")
|
||||
distroRex = func() *regexValue {
|
||||
result := ®exValue{r: regexp.MustCompile(`.*`)}
|
||||
flag.Var(result, "distro-regex", "The regex that matches what distros should be run")
|
||||
return result
|
||||
}()
|
||||
)
|
||||
var runVMTests = flag.Bool("run-vm-tests", false, "if set, run expensive (10G+ ram) VM based integration tests")
|
||||
|
||||
type Distro struct {
|
||||
name string // amazon-linux
|
||||
@@ -73,127 +46,16 @@ 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" ]`
|
||||
- [ apt-get, "-y", install, curl, "apt-transport-https", gnupg2 ]
|
||||
`
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -208,93 +70,70 @@ func fetchDistro(t *testing.T, resultDistro Distro) {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
err = fout.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("can't close fout: %v", err)
|
||||
}
|
||||
|
||||
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 != resultDistro.sha256sum {
|
||||
t.Logf("got: %q", hash)
|
||||
t.Logf("want: %q", resultDistro.sha256sum)
|
||||
t.Fatal("hash mismatch, someone is doing something nasty")
|
||||
}
|
||||
|
||||
t.Logf("hash check passed (%s)", resultDistro.sha256sum)
|
||||
}
|
||||
}
|
||||
|
||||
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.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
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.
|
||||
// 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()
|
||||
|
||||
@@ -311,13 +150,7 @@ func mkLayeredQcow(t *testing.T, tdir string, d Distro) {
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
// 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()
|
||||
|
||||
@@ -331,7 +164,7 @@ func mkSeed(t *testing.T, d Distro, sshKey, hostURL, tdir string, port int) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = metaDataTempl.Execute(fout, struct {
|
||||
err = template.Must(template.New("meta-data.yaml").Parse(metaDataTemplate)).Execute(fout, struct {
|
||||
ID string
|
||||
Hostname string
|
||||
}{
|
||||
@@ -355,20 +188,18 @@ func mkSeed(t *testing.T, d Distro, sshKey, hostURL, tdir string, port int) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = userDataTempl.Execute(fout, struct {
|
||||
err = template.Must(template.New("user-data.yaml").Parse(userDataTemplate)).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)
|
||||
@@ -380,23 +211,17 @@ func mkSeed(t *testing.T, d Distro, sshKey, hostURL, tdir string, port int) {
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{
|
||||
run(t, tdir, "genisoimage",
|
||||
"-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.
|
||||
// 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()
|
||||
|
||||
@@ -404,8 +229,9 @@ func mkVM(t *testing.T, n int, d Distro, sshKey, hostURL, tdir string) func() {
|
||||
if err != nil {
|
||||
t.Fatalf("can't find cache dir: %v", err)
|
||||
}
|
||||
cdir = filepath.Join(cdir, "tailscale", "vm-test")
|
||||
cdir = filepath.Join(cdir, "within", "mkvm")
|
||||
os.MkdirAll(filepath.Join(cdir, "qcow2"), 0755)
|
||||
os.MkdirAll(filepath.Join(cdir, "seed"), 0755)
|
||||
|
||||
port := 23100 + n
|
||||
|
||||
@@ -424,16 +250,12 @@ func mkVM(t *testing.T, n int, d Distro, sshKey, hostURL, tdir string) func() {
|
||||
"-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)
|
||||
}
|
||||
@@ -449,41 +271,43 @@ func mkVM(t *testing.T, n int, d Distro, sshKey, hostURL, tdir string) func() {
|
||||
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.
|
||||
// TestVMIntegrationEndToEnd creates a virtual machine with mkvm(1X), 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)")
|
||||
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.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.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()
|
||||
distros := []Distro{
|
||||
{"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"},
|
||||
{"centos-7", "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2", "1db30c9c272fb37b00111b93dcebff16c278384755bdbe158559e9c240b73b80", 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", "https://cdimage.debian.org/cdimage/openstack/9.13.21-20210511/debian-9.13.21-20210511-openstack-amd64.qcow2", "0667a08e2d947b331aee068db4bbf3a703e03edaf5afa52e23d534adff44b62a", 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", "3203e256dab5981ca3301408574b63bc522a69972fbe9850b65b54ff44a96e0a", 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-tumbleweed", "https://download.opensuse.org/tumbleweed/appliances/openSUSE-Tumbleweed-JeOS.x86_64-OpenStack-Cloud.qcow2", "ba3ecd281045b5019f0fb11378329a644a41870b77631ea647b128cd07eb804b", 512, "zypper"},
|
||||
{"ubuntu-16-04", "https://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-amd64-disk1.img", "50a21bc067c05e0c73bf5d8727ab61152340d93073b3dc32eff18b626f7d813b", 512, "apt"},
|
||||
{"ubuntu-18-04", "https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img", "08396cf95c18534a2e3f88289bd92d18eee76f0e75813636b3ab9f1e603816d7", 512, "apt"},
|
||||
{"ubuntu-20-04", "https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img", "513158b22ff0f08d0a078d8d60293bcddffdb17094a7809c76c52aba415ecc54", 512, "apt"},
|
||||
{"ubuntu-20-10", "https://cloud-images.ubuntu.com/groovy/current/groovy-server-cloudimg-amd64.img", "e470df72fce4fb8d0ee4ef8af8eed740ee3bf51290515eb42e5c747725e98b6d", 512, "apt"},
|
||||
{"ubuntu-21-04", "https://cloud-images.ubuntu.com/hirsute/current/hirsute-server-cloudimg-amd64.img", "7fab8eda0bcf6f8f6e63845ccf1e29de4706e3359c82d3888835093020fe6f05", 512, "apt"},
|
||||
}
|
||||
|
||||
rex := distroRex.Unwrap()
|
||||
dir := t.TempDir()
|
||||
|
||||
ln, err := net.Listen("tcp", deriveBindhost(t)+":0")
|
||||
if err != nil {
|
||||
@@ -496,7 +320,7 @@ func TestVMIntegrationEndToEnd(t *testing.T) {
|
||||
|
||||
var (
|
||||
ipMu sync.Mutex
|
||||
ipMap = map[string]ipMapping{}
|
||||
ipMap = map[string]string{} // SSH port => IP address
|
||||
)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
@@ -512,12 +336,7 @@ func TestVMIntegrationEndToEnd(t *testing.T) {
|
||||
|
||||
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}
|
||||
ipMap[name] = host
|
||||
t.Logf("%s: %v", name, host)
|
||||
})
|
||||
|
||||
@@ -543,224 +362,133 @@ func TestVMIntegrationEndToEnd(t *testing.T) {
|
||||
loginServer := fmt.Sprintf("http://%s", ln.Addr())
|
||||
t.Logf("loginServer: %s", loginServer)
|
||||
|
||||
tstest.FixLogs(t)
|
||||
defer tstest.UnfixLogs(t)
|
||||
cancels := make(chan func(), len(distros))
|
||||
|
||||
ramsem := semaphore.NewWeighted(int64(*vmRamLimit))
|
||||
bins := integration.BuildTestBinaries(t)
|
||||
|
||||
t.Run("do", func(t *testing.T) {
|
||||
t.Run("mkvm", 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)
|
||||
cancels <- cancel
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
close(cancels)
|
||||
for cancel := range cancels {
|
||||
//lint:ignore SA9001 They do actually get ran
|
||||
defer cancel()
|
||||
|
||||
// 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()
|
||||
if len(cancels) == 0 {
|
||||
t.Log("all VMs started")
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
if !working {
|
||||
t.Fatalf("can't connect to %s, tried %d times", hostport, maxRetries)
|
||||
}
|
||||
t.Run("wait-for-vms", func(t *testing.T) {
|
||||
t.Log("waiting for VMs to register")
|
||||
waiter := time.NewTicker(time.Second)
|
||||
defer waiter.Stop()
|
||||
n := 0
|
||||
for {
|
||||
<-waiter.C
|
||||
ipMu.Lock()
|
||||
if len(ipMap) == len(distros) {
|
||||
ipMu.Unlock()
|
||||
break
|
||||
} else {
|
||||
if n%30 == 0 {
|
||||
t.Logf("ipMap: %d", len(ipMap))
|
||||
t.Logf("distros: %d", len(distros))
|
||||
}
|
||||
}
|
||||
n++
|
||||
ipMu.Unlock()
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
ipMu.Lock()
|
||||
defer ipMu.Unlock()
|
||||
t.Run("join-net", func(t *testing.T) {
|
||||
for port := range ipMap {
|
||||
port := port
|
||||
t.Run(port, func(t *testing.T) {
|
||||
config := &ssh.ClientConfig{
|
||||
User: "ts",
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer), ssh.Password("hunter2")},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
timeout := 5 * time.Minute
|
||||
cli, err := ssh.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", port), config)
|
||||
if err != nil {
|
||||
t.Fatalf("can't dial 127.0.0.1:%s: %v", port, err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
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.Parallel()
|
||||
t.Logf("about to ssh into 127.0.0.1:%s", port)
|
||||
timeout := 5 * time.Minute
|
||||
|
||||
t.Log("opened session")
|
||||
e, _, err := expect.SpawnSSH(cli, timeout, expect.Verbose(true), expect.VerboseWriter(os.Stdout))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: can't register a shell session: %v", port, err)
|
||||
}
|
||||
defer e.Close()
|
||||
|
||||
_, _, 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)
|
||||
}
|
||||
}
|
||||
_, _, err = e.Expect(regexp.MustCompile(`(\$|\>)`), timeout)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: can't get a shell: %v", port, err)
|
||||
}
|
||||
t.Logf("got shell for %s", port)
|
||||
err = e.Send(fmt.Sprintf("sudo tailscale up --login-server %s\n", loginServer))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: can't send tailscale up command: %v", port, err)
|
||||
}
|
||||
_, _, err = e.Expect(regexp.MustCompile(`Success.`), timeout)
|
||||
if err != nil {
|
||||
t.Fatalf("can't extract URL: %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)
|
||||
if numNodes := cs.NumNodes(); numNodes != len(ipMap) {
|
||||
t.Errorf("wanted %d nodes, got: %d", len(ipMap), numNodes)
|
||||
}
|
||||
}
|
||||
|
||||
func deriveBindhost(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
ifName, err := interfaces.DefaultRouteInterface()
|
||||
ifaces, err := net.Interfaces()
|
||||
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
|
||||
rex := regexp.MustCompile(`^(eth|enp|wlp|wlan)`)
|
||||
|
||||
for _, iface := range ifaces {
|
||||
t.Logf("found interface %s: %d", iface.Name, iface.Flags&net.FlagUp)
|
||||
if (iface.Flags & net.FlagUp) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if rex.MatchString(iface.Name) {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
t.Fatalf("can't get address for %s: %v", iface.Name, err)
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
return netaddr.MustParseIPPrefix(addr.String()).IP().String()
|
||||
}
|
||||
}
|
||||
ret = prefix.IP().String()
|
||||
})
|
||||
if ret != "" {
|
||||
return ret
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Fatal("can't find a bindhost")
|
||||
return "unreachable"
|
||||
return "invalid"
|
||||
}
|
||||
|
||||
func TestDeriveBindhost(t *testing.T) {
|
||||
@@ -781,15 +509,12 @@ cloud_final_modules:
|
||||
- [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:
|
||||
- name: ts
|
||||
plain_text_passwd: hunter2
|
||||
groups: [ wheel ]
|
||||
sudo: [ "ALL=(ALL) NOPASSWD:ALL" ]
|
||||
shell: /bin/sh
|
||||
ssh-authorized-keys:
|
||||
- {{.SSHKey}}
|
||||
|
||||
write_files:
|
||||
@@ -801,5 +526,7 @@ write_files:
|
||||
|
||||
runcmd:
|
||||
{{.InstallPre}}
|
||||
- [ "sh", "-c", "curl https://raw.githubusercontent.com/tailscale/tailscale/Xe/test-install-script-libvirtd/scripts/installer.sh | sh" ]
|
||||
- [ systemctl, enable, --now, tailscaled.service ]
|
||||
- [ curl, "{{.HostURL}}/myip/{{.Port}}", "-H", "User-Agent: {{.Hostname}}" ]
|
||||
`
|
||||
|
||||
@@ -89,3 +89,64 @@ func (fn JSONHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Request
|
||||
w.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO() Set this function such that chunk encoding works
|
||||
// Currently the same thing with chunking headers set.
|
||||
func (fn JSONHandlerFunc) ServeHTTPChunkEncodingReturn(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Connection", "Keep-Alive")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
var resp *response
|
||||
status, data, err := fn(r)
|
||||
if err != nil {
|
||||
if werr, ok := err.(HTTPError); ok {
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: werr.Msg,
|
||||
Data: data,
|
||||
}
|
||||
// Unwrap the HTTPError here because we are communicating with
|
||||
// the client in this handler. We don't want the wrapping
|
||||
// ReturnHandler to do it too.
|
||||
err = werr.Err
|
||||
if werr.Msg != "" {
|
||||
err = fmt.Errorf("%s: %w", werr.Msg, err)
|
||||
}
|
||||
// take status from the HTTPError to encourage error handling in one location
|
||||
if status != 0 && status != werr.Code {
|
||||
err = fmt.Errorf("[unexpected] non-zero status that does not match HTTPError status, status: %d, HTTPError.code: %d: %w", status, werr.Code, err)
|
||||
}
|
||||
status = werr.Code
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
}
|
||||
} else if status == 0 {
|
||||
status = http.StatusInternalServerError
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
} else if err == nil {
|
||||
resp = &response{
|
||||
Status: "success",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
b, jerr := json.Marshal(resp)
|
||||
if jerr != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"status":"error","error":"json marshal error"}`))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w, and then we could not respond: %v", err, jerr)
|
||||
}
|
||||
return jerr
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
9
version/GENERATE.go
Normal 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
2
version/all.do
Normal file
@@ -0,0 +1,2 @@
|
||||
redo-ifchange ver.go version.xcconfig version.h
|
||||
|
||||
1
version/clean.do
Normal file
1
version/clean.do
Normal 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
102
version/mkversion_test.go
Normal 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
9
version/ver.go.do
Normal 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
14
version/ver.go.in
Normal 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}"
|
||||
3
version/version-info.sh.do
Normal file
3
version/version-info.sh.do
Normal file
@@ -0,0 +1,3 @@
|
||||
./version.sh ../.. >$3
|
||||
redo-always
|
||||
redo-stamp <$3
|
||||
@@ -2,30 +2,31 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !redo,!xversion
|
||||
|
||||
// Package version provides the version that the binary was built at.
|
||||
package version
|
||||
|
||||
// Long is a full version number for this build, of the form
|
||||
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
|
||||
// provided.
|
||||
var Long = "date.20210603"
|
||||
const Long = "date.20210505"
|
||||
|
||||
// Short is a short version number for this build, of the form
|
||||
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
|
||||
var Short = ""
|
||||
const Short = Long
|
||||
|
||||
func init() {
|
||||
if Short == "" {
|
||||
// If it hasn't been link-stamped with -X:
|
||||
Short = Long
|
||||
}
|
||||
}
|
||||
// LONG is a deprecated alias for Long. Don't use it.
|
||||
const LONG = Long
|
||||
|
||||
// SHORT is a deprecated alias for Short. Don't use it.
|
||||
const SHORT = Short
|
||||
|
||||
// GitCommit, if non-empty, is the git commit of the
|
||||
// github.com/tailscale/tailscale repository at which Tailscale was
|
||||
// built. Its format is the one returned by `git describe --always
|
||||
// --exclude "*" --dirty --abbrev=200`.
|
||||
var GitCommit = ""
|
||||
const GitCommit = ""
|
||||
|
||||
// ExtraGitCommit, if non-empty, is the git commit of a "supplemental"
|
||||
// repository at which Tailscale was built. Its format is the same as
|
||||
@@ -37,4 +38,4 @@ var GitCommit = ""
|
||||
// Android OSS repository). Together, GitCommit and ExtraGitCommit
|
||||
// exactly describe what repositories and commits were used in a
|
||||
// build.
|
||||
var ExtraGitCommit = ""
|
||||
const ExtraGitCommit = ""
|
||||
|
||||
9
version/version.h.do
Normal file
9
version/version.h.do
Normal file
@@ -0,0 +1,9 @@
|
||||
redo-ifchange version-info.sh
|
||||
|
||||
. ./version-info.sh
|
||||
|
||||
cat >$3 <<EOF
|
||||
#define TAILSCALE_VERSION_LONG "$VERSION_LONG"
|
||||
#define TAILSCALE_VERSION_SHORT "$VERSION_SHORT"
|
||||
#define TAILSCALE_VERSION_WIN_RES $VERSION_WINRES
|
||||
EOF
|
||||
130
version/version.sh
Executable file
130
version/version.sh
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Return the commitid of the given ref in the given repo dir. If the worktree
|
||||
# or index is dirty, also appends -dirty.
|
||||
#
|
||||
# $ git_hash_dirty ../.. HEAD
|
||||
# 1be01ddc6e430ca3aa9beea3587d16750efb3241-dirty
|
||||
git_hash_dirty() {
|
||||
(
|
||||
cd "$1"
|
||||
x=$(git rev-parse HEAD)
|
||||
if ! git diff-index --quiet HEAD; then
|
||||
x="$x-dirty"
|
||||
fi
|
||||
echo "$x"
|
||||
)
|
||||
}
|
||||
|
||||
case $# in
|
||||
0|1)
|
||||
# extra_hash_or_dir is either:
|
||||
# - a git commitid
|
||||
# or
|
||||
# - the path to a git repo from which to calculate the real hash.
|
||||
#
|
||||
# It gets embedded as an additional commit hash in built
|
||||
# binaries, to help us locate the exact set of tools and code
|
||||
# that were used.
|
||||
extra_hash_or_dir="${1:-}"
|
||||
if [ -z "$extra_hash_or_dir" ]; then
|
||||
# Nothing, empty extra hash is fine.
|
||||
extra_hash=""
|
||||
elif [ -e "$extra_hash_or_dir/.git" ]; then
|
||||
extra_hash=$(git_hash_dirty "$extra_hash_or_dir" HEAD)
|
||||
elif ! expr "$extra_hash_or_dir" : "^[0-9a-f]*$"; then
|
||||
echo "Invalid extra hash '$extra_hash_or_dir', must be a git commit or path to a git repo" >&2
|
||||
exit 1
|
||||
else
|
||||
extra_hash="$extra_hash_or_dir"
|
||||
fi
|
||||
|
||||
# Load the base version and optional corresponding git hash
|
||||
# from the VERSION file. If there is no git hash in the file,
|
||||
# we use the hash of the last change to the VERSION file.
|
||||
version_file="$(dirname $0)/../VERSION.txt"
|
||||
IFS=".$IFS" read -r major minor patch base_git_hash <"$version_file"
|
||||
if [ -z "$base_git_hash" ]; then
|
||||
base_git_hash=$(git rev-list --max-count=1 HEAD -- "$version_file")
|
||||
fi
|
||||
|
||||
git_hash=$(git_hash_dirty . HEAD)
|
||||
# The number of extra commits between the release base to git_hash.
|
||||
change_count=$(git rev-list --count HEAD "^$base_git_hash")
|
||||
;;
|
||||
6)
|
||||
# Test mode: rather than run git commands and whatnot, take in
|
||||
# all the version pieces as arguments.
|
||||
git_hash=$1
|
||||
extra_hash=$2
|
||||
major=$3
|
||||
minor=$4
|
||||
patch=$5
|
||||
change_count=$6
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [extra-git-commitid-or-dir]"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Shortened versions of git hashes, so that they fit neatly into an
|
||||
# "elongated" but still human-readable version number.
|
||||
short_git_hash=$(echo "$git_hash" | cut -c1-9)
|
||||
short_extra_hash=$(echo "$extra_hash" | cut -c1-9)
|
||||
|
||||
# Convert major/minor/patch/change_count into an adjusted
|
||||
# major/minor/patch. This block is where all our policies on
|
||||
# versioning are.
|
||||
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
|
||||
# Odd minor numbers are unstable builds.
|
||||
if [ "$patch" != "0" ]; then
|
||||
# This is a fatal error, because a non-zero patch number
|
||||
# indicates that we created an unstable VERSION.txt in violation
|
||||
# of our versioning policy, and we want to blow up loudly to
|
||||
# get that fixed.
|
||||
echo "Unstable release $major.$minor.$patch has a non-zero patch number, which is not allowed" >&2
|
||||
exit 1
|
||||
fi
|
||||
patch="$change_count"
|
||||
change_suffix=""
|
||||
elif [ "$change_count" != "0" ]; then
|
||||
# Even minor numbers are stable builds, but stable builds are
|
||||
# supposed to have a zero change count. Therefore, we're currently
|
||||
# describing a commit that's on a release branch, but hasn't been
|
||||
# tagged as a patch release yet.
|
||||
#
|
||||
# We used to change the version number to 0.0.0 in that case, but that
|
||||
# caused some features to get disabled due to the low version number.
|
||||
# Instead, add yet another suffix to the version number, with a change
|
||||
# count.
|
||||
change_suffix="-$change_count"
|
||||
else
|
||||
# Even minor number with no extra changes.
|
||||
change_suffix=""
|
||||
fi
|
||||
|
||||
# Hack for 1.1: add 1000 to the patch number. We switched from using
|
||||
# the proprietary repo's change_count over to using the OSS repo's
|
||||
# change_count, and this was necessary to avoid a backwards jump in
|
||||
# release numbers.
|
||||
if [ "$major.$minor" = "1.1" ]; then
|
||||
patch="$((patch + 1000))"
|
||||
fi
|
||||
|
||||
# At this point, the version number correctly reflects our
|
||||
# policies. All that remains is to output the various vars that other
|
||||
# code can use to embed version data.
|
||||
if [ -z "$extra_hash" ]; then
|
||||
long_version_suffix="$change_suffix-t$short_git_hash"
|
||||
else
|
||||
long_version_suffix="$change_suffix-t$short_git_hash-g$short_extra_hash"
|
||||
fi
|
||||
cat <<EOF
|
||||
VERSION_SHORT="$major.$minor.$patch"
|
||||
VERSION_LONG="$major.$minor.$patch$long_version_suffix"
|
||||
VERSION_GIT_HASH="$git_hash"
|
||||
VERSION_EXTRA_HASH="$extra_hash"
|
||||
VERSION_XCODE="$((major + 100)).$minor.$patch"
|
||||
VERSION_WINRES="$major,$minor,$patch,0"
|
||||
EOF
|
||||
14
version/version.xcconfig.do
Normal file
14
version/version.xcconfig.do
Normal file
@@ -0,0 +1,14 @@
|
||||
redo-ifchange version-info.sh
|
||||
|
||||
. ./version-info.sh
|
||||
|
||||
# CFBundleShortVersionString: the "short name" used in the App Store.
|
||||
# eg. 0.92.98
|
||||
echo "VERSION_NAME = $VERSION_SHORT"
|
||||
# CFBundleVersion: the build number. Needs to be 3 numeric sections
|
||||
# that increment for each release according to SemVer rules.
|
||||
#
|
||||
# We start counting at 100 because we submitted using raw build
|
||||
# numbers before, and Apple doesn't let you start over. e.g. 0.98.3
|
||||
# -> 100.98.3
|
||||
echo "VERSION_ID = $VERSION_XCODE"
|
||||
17
version/xversion.go
Normal file
17
version/xversion.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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,xversion
|
||||
|
||||
package version
|
||||
|
||||
// Replaced at build time with the Go linker flag -X. See
|
||||
// ../build_dist.sh for example usage, and version.go for field
|
||||
// documentation.
|
||||
var Long string = "<not set>"
|
||||
var Short string = "<not set>"
|
||||
var LONG = Long
|
||||
var SHORT = Short
|
||||
var GitCommit = ""
|
||||
var ExtraGitCommit = ""
|
||||
@@ -130,8 +130,7 @@ func NewAllowAllForTest(logf logger.Logf) *Filter {
|
||||
var sb netaddr.IPSetBuilder
|
||||
sb.AddPrefix(any4)
|
||||
sb.AddPrefix(any6)
|
||||
ipSet, _ := sb.IPSet()
|
||||
return New(ms, ipSet, ipSet, nil, logf)
|
||||
return New(ms, sb.IPSet(), sb.IPSet(), nil, logf)
|
||||
}
|
||||
|
||||
// NewAllowNone returns a packet filter that rejects everything.
|
||||
|
||||
@@ -54,10 +54,7 @@ func newFilter(logf logger.Logf) *Filter {
|
||||
|
||||
var logB netaddr.IPSetBuilder
|
||||
logB.Complement()
|
||||
localNetsSet, _ := localNets.IPSet()
|
||||
logBSet, _ := logB.IPSet()
|
||||
|
||||
return New(matches, localNetsSet, logBSet, nil, logf)
|
||||
return New(matches, localNets.IPSet(), logB.IPSet(), nil, logf)
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
@@ -427,7 +424,7 @@ func TestLoggingPrivacy(t *testing.T) {
|
||||
logB.AddPrefix(netaddr.MustParseIPPrefix("100.64.0.0/10"))
|
||||
logB.AddPrefix(tsaddr.TailscaleULARange())
|
||||
f := newFilter(logf)
|
||||
f.logIPs, _ = logB.IPSet()
|
||||
f.logIPs = logB.IPSet()
|
||||
|
||||
var (
|
||||
ts4 = netaddr.IPPortFrom(tsaddr.CGNATRange().IP().Next(), 1234)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
@@ -824,6 +825,7 @@ func (c *Conn) LastRecvActivityOfDisco(dk tailcfg.DiscoKey) time.Time {
|
||||
|
||||
// Ping handles a "tailscale ping" CLI query.
|
||||
func (c *Conn) Ping(peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) {
|
||||
log.Println("CLIPING")
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.privateKey.IsZero() {
|
||||
@@ -2212,6 +2214,7 @@ func nodesEqual(x, y []*tailcfg.Node) bool {
|
||||
// conditionally sent to SetDERPMap instead.
|
||||
func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
|
||||
c.mu.Lock()
|
||||
log.Println("NETMAP being set")
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.netMap != nil && nodesEqual(c.netMap.Peers, nm.Peers) {
|
||||
@@ -2869,6 +2872,26 @@ func (c *RebindingUDPConn) closeLocked() error {
|
||||
return c.pconn.Close()
|
||||
}
|
||||
|
||||
func (c *RebindingUDPConn) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) {
|
||||
for {
|
||||
c.mu.Lock()
|
||||
pconn := c.pconn
|
||||
c.mu.Unlock()
|
||||
|
||||
n, err := pconn.WriteTo(b, addr)
|
||||
if err != nil {
|
||||
c.mu.Lock()
|
||||
pconn2 := c.pconn
|
||||
c.mu.Unlock()
|
||||
|
||||
if pconn != pconn2 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RebindingUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
for {
|
||||
c.mu.Lock()
|
||||
@@ -3370,6 +3393,7 @@ func (de *discoEndpoint) send(b []byte) error {
|
||||
now := time.Now()
|
||||
|
||||
de.mu.Lock()
|
||||
log.Println("Discosend")
|
||||
udpAddr, derpAddr := de.addrForSendLocked(now)
|
||||
if udpAddr.IsZero() || now.After(de.trustBestAddrUntil) {
|
||||
de.sendPingsLocked(now, true)
|
||||
@@ -3428,7 +3452,9 @@ func (de *discoEndpoint) removeSentPingLocked(txid stun.TxID, sp sentPing) {
|
||||
// The caller (startPingLocked) should've already been recorded the ping in
|
||||
// sentPing and set up the timer.
|
||||
func (de *discoEndpoint) sendDiscoPing(ep netaddr.IPPort, txid stun.TxID, logLevel discoLogLevel) {
|
||||
log.Println("sendDiscoPing")
|
||||
sent, _ := de.sendDiscoMessage(ep, &disco.Ping{TxID: [12]byte(txid)}, logLevel)
|
||||
log.Println(sent)
|
||||
if !sent {
|
||||
de.forgetPing(txid)
|
||||
}
|
||||
@@ -3607,6 +3633,7 @@ func (de *discoEndpoint) noteConnectivityChange() {
|
||||
// It should be called with the Conn.mu held.
|
||||
func (de *discoEndpoint) handlePongConnLocked(m *disco.Pong, src netaddr.IPPort) {
|
||||
de.mu.Lock()
|
||||
log.Println("Disco Reached")
|
||||
defer de.mu.Unlock()
|
||||
|
||||
isDerp := src.IP() == derpMagicIPAddr
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@@ -307,6 +308,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
|
||||
e.wgLogger = wglog.NewLogger(logf)
|
||||
e.tundev.OnTSMPPongReceived = func(pong packet.TSMPPongReply) {
|
||||
log.Println("PONGReceived")
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
cb := e.pongCallback[pong.Data]
|
||||
@@ -369,6 +371,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
|
||||
// echoRespondToAll is an inbound post-filter responding to all echo requests.
|
||||
func echoRespondToAll(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
|
||||
log.Println("ECHO respond to all")
|
||||
if p.IsEchoRequest() {
|
||||
header := p.ICMP4Header()
|
||||
header.ToResponse()
|
||||
@@ -1087,6 +1090,7 @@ func (e *userspaceEngine) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) SetNetworkMap(nm *netmap.NetworkMap) {
|
||||
log.Println("SETNETMAP")
|
||||
e.magicConn.SetNetworkMap(nm)
|
||||
e.mu.Lock()
|
||||
e.netMap = nm
|
||||
@@ -1123,15 +1127,18 @@ func (e *userspaceEngine) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult)) {
|
||||
log.Println("Userspace Ping Called")
|
||||
res := &ipnstate.PingResult{IP: ip.String()}
|
||||
peer, err := e.peerForIP(ip)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
e.logf("ping(%v): %v", ip, err)
|
||||
res.Err = err.Error()
|
||||
cb(res)
|
||||
return
|
||||
}
|
||||
if peer == nil {
|
||||
log.Println("No peer moment")
|
||||
e.logf("ping(%v): no matching peer", ip)
|
||||
res.Err = "no matching peer"
|
||||
cb(res)
|
||||
@@ -1139,6 +1146,7 @@ func (e *userspaceEngine) Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.Pi
|
||||
}
|
||||
pingType := "disco"
|
||||
if useTSMP {
|
||||
log.Println("TSMPSELECTED")
|
||||
pingType = "TSMP"
|
||||
}
|
||||
e.logf("ping(%v): sending %v ping to %v %v ...", ip, pingType, peer.Key.ShortString(), peer.ComputedName)
|
||||
@@ -1167,8 +1175,10 @@ func (e *userspaceEngine) mySelfIPMatchingFamily(dst netaddr.IP) (src netaddr.IP
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) sendTSMPPing(ip netaddr.IP, peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) {
|
||||
log.Println("TSMPcheck")
|
||||
srcIP, err := e.mySelfIPMatchingFamily(ip)
|
||||
if err != nil {
|
||||
log.Println("TSMPcheckerror")
|
||||
res.Err = err.Error()
|
||||
cb(res)
|
||||
return
|
||||
@@ -1190,12 +1200,17 @@ func (e *userspaceEngine) sendTSMPPing(ip netaddr.IP, peer *tailcfg.Node, res *i
|
||||
|
||||
var data [8]byte
|
||||
crand.Read(data[:])
|
||||
log.Println("CRAND CHECKlog")
|
||||
fmt.Println("CRAND CHECKfmt")
|
||||
|
||||
expireTimer := time.AfterFunc(10*time.Second, func() {
|
||||
log.Println("CHECKEXPIRE")
|
||||
e.setTSMPPongCallback(data, nil)
|
||||
})
|
||||
log.Println("TIMECHECK")
|
||||
t0 := time.Now()
|
||||
e.setTSMPPongCallback(data, func(pong packet.TSMPPongReply) {
|
||||
log.Println("ping cb called")
|
||||
expireTimer.Stop()
|
||||
d := time.Since(t0)
|
||||
res.LatencySeconds = d.Seconds()
|
||||
@@ -1208,22 +1223,32 @@ func (e *userspaceEngine) sendTSMPPing(ip netaddr.IP, peer *tailcfg.Node, res *i
|
||||
var tsmpPayload [9]byte
|
||||
tsmpPayload[0] = byte(packet.TSMPTypePing)
|
||||
copy(tsmpPayload[1:], data[:])
|
||||
log.Println("PAYLOADCHECK")
|
||||
|
||||
tsmpPing := packet.Generate(iph, tsmpPayload[:])
|
||||
log.Println("BEFOREPACKET", tsmpPing)
|
||||
log.Println("PACKETGEN", *res, res.LatencySeconds)
|
||||
e.tundev.InjectOutbound(tsmpPing)
|
||||
log.Println("TUNDEVINJECT")
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) setTSMPPongCallback(data [8]byte, cb func(packet.TSMPPongReply)) {
|
||||
log.Println("Ponger2nolock", data)
|
||||
e.mu.Lock()
|
||||
log.Println("Ponger2", e.pongCallback == nil, cb == nil)
|
||||
defer e.mu.Unlock()
|
||||
if e.pongCallback == nil {
|
||||
log.Println("pongCallback nil")
|
||||
e.pongCallback = map[[8]byte]func(packet.TSMPPongReply){}
|
||||
}
|
||||
if cb == nil {
|
||||
log.Println("DELETEoccur")
|
||||
delete(e.pongCallback, data)
|
||||
} else {
|
||||
log.Println("Callbackset")
|
||||
e.pongCallback[data] = cb
|
||||
}
|
||||
log.Println("PONGCALLBACKMAP", data, e.pongCallback)
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) RegisterIPPortIdentity(ipport netaddr.IPPort, tsIP netaddr.IP) {
|
||||
@@ -1291,12 +1316,20 @@ func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, err error)
|
||||
// Check for exact matches before looking for subnet matches.
|
||||
var bestInNMPrefix netaddr.IPPrefix
|
||||
var bestInNM *tailcfg.Node
|
||||
log.Println("Scan starting : ", len(nm.Peers), nm.Addresses)
|
||||
for _, p := range nm.Peers {
|
||||
log.Println("peerp", p.Addresses, p.AllowedIPs, p.ID)
|
||||
for _, a := range p.Addresses {
|
||||
log.Println("paddr", a)
|
||||
if a.IP() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) {
|
||||
log.Println("Foundp")
|
||||
return p, nil
|
||||
} else {
|
||||
// log.Println("Failure : ", a.IP(), a.IsSingleIP(), tsaddr.IsTailscaleIP(ip))
|
||||
}
|
||||
}
|
||||
log.Println("ALLOW : ", p.AllowedIPs)
|
||||
bestInNM = p
|
||||
for _, cidr := range p.AllowedIPs {
|
||||
if !cidr.Contains(ip) {
|
||||
continue
|
||||
@@ -1310,6 +1343,7 @@ func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, err error)
|
||||
|
||||
e.wgLock.Lock()
|
||||
defer e.wgLock.Unlock()
|
||||
log.Println("Scanpoint2")
|
||||
|
||||
// TODO(bradfitz): this is O(n peers). Add ART to netaddr?
|
||||
var best netaddr.IPPrefix
|
||||
@@ -1334,10 +1368,13 @@ func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, err error)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Println("Scanpoint3")
|
||||
if bestInNM == nil {
|
||||
log.Println("Scanpoint4")
|
||||
return nil, nil
|
||||
}
|
||||
if bestInNMPrefix.Bits() == 0 {
|
||||
log.Println("Scanpoint5")
|
||||
return nil, errors.New("exit node found but not enabled")
|
||||
}
|
||||
return nil, fmt.Errorf("node %q found, but not using its %v route", bestInNM.ComputedNameWithHost, bestInNMPrefix)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package wglog
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -120,20 +121,17 @@ func (x *Logger) SetPeers(peers []wgcfg.Peer) {
|
||||
|
||||
// wireguardGoString prints p in the same format used by wireguard-go.
|
||||
func wireguardGoString(k wgkey.Key) string {
|
||||
src := k
|
||||
b64 := func(input byte) byte {
|
||||
return input + 'A' + byte(((25-int(input))>>8)&6) - byte(((51-int(input))>>8)&75) - byte(((61-int(input))>>8)&15) + byte(((62-int(input))>>8)&3)
|
||||
}
|
||||
b := []byte("peer(____…____)")
|
||||
const first = len("peer(")
|
||||
const second = len("peer(____…")
|
||||
b[first+0] = b64((src[0] >> 2) & 63)
|
||||
b[first+1] = b64(((src[0] << 4) | (src[1] >> 4)) & 63)
|
||||
b[first+2] = b64(((src[1] << 2) | (src[2] >> 6)) & 63)
|
||||
b[first+3] = b64(src[2] & 63)
|
||||
b[second+0] = b64(src[29] & 63)
|
||||
b[second+1] = b64((src[30] >> 2) & 63)
|
||||
b[second+2] = b64(((src[30] << 4) | (src[31] >> 4)) & 63)
|
||||
b[second+3] = b64((src[31] << 2) & 63)
|
||||
return string(b)
|
||||
const prefix = "peer("
|
||||
b := make([]byte, len(prefix)+44)
|
||||
copy(b, prefix)
|
||||
r := b[len(prefix):]
|
||||
base64.StdEncoding.Encode(r, k[:])
|
||||
r = r[4:]
|
||||
copy(r, "…")
|
||||
r = r[len("…"):]
|
||||
copy(r, b[len(prefix)+39:len(prefix)+43])
|
||||
r = r[4:]
|
||||
r[0] = ')'
|
||||
r = r[1:]
|
||||
return string(b[:len(b)-len(r)])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user