mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-14 06:57:31 +00:00
feature/taildrop: add integration test
Taildrop has never had an end-to-end test since it was introduced.
This adds a basic one.
It caught two recent refactoring bugs & one from 2022 (0f7da5c7dc
).
This is prep for moving the rest of Taildrop out of LocalBackend, so
we can do more refactorings with some confidence.
Updates #15812
Change-Id: I6182e49c5641238af0bfdd9fea1ef0420c112738
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:

committed by
Brad Fitzpatrick

parent
a0d7c81a27
commit
e415f51351
@@ -33,6 +33,7 @@ import (
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/ipn"
|
||||
@@ -436,6 +437,7 @@ func NewTestEnv(t testing.TB, opts ...TestEnvOpt) *TestEnv {
|
||||
derpMap := RunDERPAndSTUN(t, logger.Discard, "127.0.0.1")
|
||||
logc := new(LogCatcher)
|
||||
control := &testcontrol.Server{
|
||||
Logf: logger.WithPrefix(t.Logf, "testcontrol: "),
|
||||
DERPMap: derpMap,
|
||||
}
|
||||
control.HTTPTestServer = httptest.NewUnstartedServer(control)
|
||||
@@ -484,6 +486,7 @@ type TestNode struct {
|
||||
|
||||
mu sync.Mutex
|
||||
onLogLine []func([]byte)
|
||||
lc *local.Client
|
||||
}
|
||||
|
||||
// NewTestNode allocates a temp directory for a new test node.
|
||||
@@ -500,14 +503,18 @@ func NewTestNode(t *testing.T, env *TestEnv) *TestNode {
|
||||
env: env,
|
||||
dir: dir,
|
||||
sockFile: sockFile,
|
||||
stateFile: filepath.Join(dir, "tailscale.state"),
|
||||
stateFile: filepath.Join(dir, "tailscaled.state"), // matches what cmd/tailscaled uses
|
||||
}
|
||||
|
||||
// Look for a data race. Once we see the start marker, start logging the rest.
|
||||
// Look for a data race or panic.
|
||||
// Once we see the start marker, start logging the rest.
|
||||
var sawRace bool
|
||||
var sawPanic bool
|
||||
n.addLogLineHook(func(line []byte) {
|
||||
lineB := mem.B(line)
|
||||
if mem.Contains(lineB, mem.S("DEBUG-ADDR=")) {
|
||||
t.Log(strings.TrimSpace(string(line)))
|
||||
}
|
||||
if mem.Contains(lineB, mem.S("WARNING: DATA RACE")) {
|
||||
sawRace = true
|
||||
}
|
||||
@@ -522,6 +529,20 @@ func NewTestNode(t *testing.T, env *TestEnv) *TestNode {
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *TestNode) LocalClient() *local.Client {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
if n.lc == nil {
|
||||
tr := &http.Transport{}
|
||||
n.lc = &local.Client{
|
||||
Socket: n.sockFile,
|
||||
UseSocketOnly: true,
|
||||
}
|
||||
n.env.t.Cleanup(tr.CloseIdleConnections)
|
||||
}
|
||||
return n.lc
|
||||
}
|
||||
|
||||
func (n *TestNode) diskPrefs() *ipn.Prefs {
|
||||
t := n.env.t
|
||||
t.Helper()
|
||||
@@ -668,9 +689,10 @@ func (n *TestNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
|
||||
t := n.env.t
|
||||
cmd := exec.Command(n.env.daemon)
|
||||
cmd.Args = append(cmd.Args,
|
||||
"--state="+n.stateFile,
|
||||
"--statedir="+n.dir,
|
||||
"--socket="+n.sockFile,
|
||||
"--socks5-server=localhost:0",
|
||||
"--debug=localhost:0",
|
||||
)
|
||||
if *verboseTailscaled {
|
||||
cmd.Args = append(cmd.Args, "-verbose=2")
|
||||
|
@@ -278,15 +278,20 @@ func TestOneNodeUpAuth(t *testing.T) {
|
||||
t.Logf("Running up --login-server=%s ...", env.ControlURL())
|
||||
|
||||
cmd := n1.Tailscale("up", "--login-server="+env.ControlURL())
|
||||
var authCountAtomic int32
|
||||
var authCountAtomic atomic.Int32
|
||||
cmd.Stdout = &authURLParserWriter{fn: func(urlStr string) error {
|
||||
t.Logf("saw auth URL %q", urlStr)
|
||||
if env.Control.CompleteAuth(urlStr) {
|
||||
atomic.AddInt32(&authCountAtomic, 1)
|
||||
if authCountAtomic.Add(1) > 1 {
|
||||
err := errors.New("completed multple auth URLs")
|
||||
t.Error(err)
|
||||
return err
|
||||
}
|
||||
t.Logf("completed auth path %s", urlStr)
|
||||
return nil
|
||||
}
|
||||
err := fmt.Errorf("Failed to complete auth path to %q", urlStr)
|
||||
t.Log(err)
|
||||
t.Error(err)
|
||||
return err
|
||||
}}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
@@ -297,7 +302,7 @@ func TestOneNodeUpAuth(t *testing.T) {
|
||||
|
||||
n1.AwaitRunning()
|
||||
|
||||
if n := atomic.LoadInt32(&authCountAtomic); n != 1 {
|
||||
if n := authCountAtomic.Load(); n != 1 {
|
||||
t.Errorf("Auth URLs completed = %d; want 1", n)
|
||||
}
|
||||
|
||||
|
@@ -55,6 +55,10 @@ type Server struct {
|
||||
MagicDNSDomain string
|
||||
HandleC2N http.Handler // if non-nil, used for /some-c2n-path/ in tests
|
||||
|
||||
// AllNodesSameUser, if true, makes all created nodes
|
||||
// belong to the same user.
|
||||
AllNodesSameUser bool
|
||||
|
||||
// ExplicitBaseURL or HTTPTestServer must be set.
|
||||
ExplicitBaseURL string // e.g. "http://127.0.0.1:1234" with no trailing URL
|
||||
HTTPTestServer *httptest.Server // if non-nil, used to get BaseURL
|
||||
@@ -96,9 +100,9 @@ type Server struct {
|
||||
logins map[key.NodePublic]*tailcfg.Login
|
||||
updates map[tailcfg.NodeID]chan updateType
|
||||
authPath map[string]*AuthPath
|
||||
nodeKeyAuthed map[key.NodePublic]bool // key => true once authenticated
|
||||
msgToSend map[key.NodePublic]any // value is *tailcfg.PingRequest or entire *tailcfg.MapResponse
|
||||
allExpired bool // All nodes will be told their node key is expired.
|
||||
nodeKeyAuthed set.Set[key.NodePublic]
|
||||
msgToSend map[key.NodePublic]any // value is *tailcfg.PingRequest or entire *tailcfg.MapResponse
|
||||
allExpired bool // All nodes will be told their node key is expired.
|
||||
}
|
||||
|
||||
// BaseURL returns the server's base URL, without trailing slash.
|
||||
@@ -522,6 +526,10 @@ func (s *Server) getUser(nodeKey key.NodePublic) (*tailcfg.User, *tailcfg.Login)
|
||||
return u, s.logins[nodeKey]
|
||||
}
|
||||
id := tailcfg.UserID(len(s.users) + 1)
|
||||
if s.AllNodesSameUser {
|
||||
id = 123
|
||||
}
|
||||
s.logf("Created user %v for node %s", id, nodeKey)
|
||||
loginName := fmt.Sprintf("user-%d@%s", id, domain)
|
||||
displayName := fmt.Sprintf("User %d", id)
|
||||
login := &tailcfg.Login{
|
||||
@@ -582,10 +590,8 @@ func (s *Server) CompleteAuth(authPathOrURL string) bool {
|
||||
if ap.nodeKey.IsZero() {
|
||||
panic("zero AuthPath.NodeKey")
|
||||
}
|
||||
if s.nodeKeyAuthed == nil {
|
||||
s.nodeKeyAuthed = map[key.NodePublic]bool{}
|
||||
}
|
||||
s.nodeKeyAuthed[ap.nodeKey] = true
|
||||
s.nodeKeyAuthed.Make()
|
||||
s.nodeKeyAuthed.Add(ap.nodeKey)
|
||||
ap.CompleteSuccessfully()
|
||||
return true
|
||||
}
|
||||
@@ -645,36 +651,40 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key.
|
||||
if s.nodes == nil {
|
||||
s.nodes = map[key.NodePublic]*tailcfg.Node{}
|
||||
}
|
||||
|
||||
_, ok := s.nodes[nk]
|
||||
machineAuthorized := true // TODO: add Server.RequireMachineAuth
|
||||
if !ok {
|
||||
|
||||
v4Prefix := netip.PrefixFrom(netaddr.IPv4(100, 64, uint8(tailcfg.NodeID(user.ID)>>8), uint8(tailcfg.NodeID(user.ID))), 32)
|
||||
v6Prefix := netip.PrefixFrom(tsaddr.Tailscale4To6(v4Prefix.Addr()), 128)
|
||||
nodeID := len(s.nodes) + 1
|
||||
v4Prefix := netip.PrefixFrom(netaddr.IPv4(100, 64, uint8(nodeID>>8), uint8(nodeID)), 32)
|
||||
v6Prefix := netip.PrefixFrom(tsaddr.Tailscale4To6(v4Prefix.Addr()), 128)
|
||||
|
||||
allowedIPs := []netip.Prefix{
|
||||
v4Prefix,
|
||||
v6Prefix,
|
||||
}
|
||||
|
||||
s.nodes[nk] = &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(user.ID),
|
||||
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(user.ID))),
|
||||
User: user.ID,
|
||||
Machine: mkey,
|
||||
Key: req.NodeKey,
|
||||
MachineAuthorized: machineAuthorized,
|
||||
Addresses: allowedIPs,
|
||||
AllowedIPs: allowedIPs,
|
||||
Hostinfo: req.Hostinfo.View(),
|
||||
Name: req.Hostinfo.Hostname,
|
||||
Capabilities: []tailcfg.NodeCapability{
|
||||
tailcfg.CapabilityHTTPS,
|
||||
tailcfg.NodeAttrFunnel,
|
||||
tailcfg.CapabilityFunnelPorts + "?ports=8080,443",
|
||||
},
|
||||
allowedIPs := []netip.Prefix{
|
||||
v4Prefix,
|
||||
v6Prefix,
|
||||
}
|
||||
node := &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(nodeID),
|
||||
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(nodeID))),
|
||||
User: user.ID,
|
||||
Machine: mkey,
|
||||
Key: req.NodeKey,
|
||||
MachineAuthorized: machineAuthorized,
|
||||
Addresses: allowedIPs,
|
||||
AllowedIPs: allowedIPs,
|
||||
Hostinfo: req.Hostinfo.View(),
|
||||
Name: req.Hostinfo.Hostname,
|
||||
Capabilities: []tailcfg.NodeCapability{
|
||||
tailcfg.CapabilityHTTPS,
|
||||
tailcfg.NodeAttrFunnel,
|
||||
tailcfg.CapabilityFileSharing,
|
||||
tailcfg.CapabilityFunnelPorts + "?ports=8080,443",
|
||||
},
|
||||
}
|
||||
s.nodes[nk] = node
|
||||
}
|
||||
requireAuth := s.RequireAuth
|
||||
if requireAuth && s.nodeKeyAuthed[nk] {
|
||||
if requireAuth && s.nodeKeyAuthed.Contains(nk) {
|
||||
requireAuth = false
|
||||
}
|
||||
allExpired := s.allExpired
|
||||
@@ -951,7 +961,6 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
|
||||
node.CapMap = nodeCapMap
|
||||
node.Capabilities = append(node.Capabilities, tailcfg.NodeAttrDisableUPnP)
|
||||
|
||||
user, _ := s.getUser(nk)
|
||||
t := time.Date(2020, 8, 3, 0, 0, 0, 1, time.UTC)
|
||||
dns := s.DNSConfig
|
||||
if dns != nil && s.MagicDNSDomain != "" {
|
||||
@@ -1013,7 +1022,7 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
|
||||
})
|
||||
res.UserProfiles = s.allUserProfiles()
|
||||
|
||||
v4Prefix := netip.PrefixFrom(netaddr.IPv4(100, 64, uint8(tailcfg.NodeID(user.ID)>>8), uint8(tailcfg.NodeID(user.ID))), 32)
|
||||
v4Prefix := netip.PrefixFrom(netaddr.IPv4(100, 64, uint8(node.ID>>8), uint8(node.ID)), 32)
|
||||
v6Prefix := netip.PrefixFrom(tsaddr.Tailscale4To6(v4Prefix.Addr()), 128)
|
||||
|
||||
res.Node.Addresses = []netip.Prefix{
|
||||
|
Reference in New Issue
Block a user