mirror of
https://github.com/tailscale/tailscale.git
synced 2026-04-03 14:13:12 -04:00
tsnet: add test for advertising multiple Services
This is a regression test for #19166, in which it was discovered that
after calling Server.ListenService for multiple Services, only the
Service from the most recent call would be advertised.
The bug was fixed in 99f8039101
Updates #19166
Signed-off-by: Harry Harpham <harry@tailscale.com>
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -59,6 +60,7 @@
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
@@ -940,6 +942,93 @@ func TestFunnelClose(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// setUpServiceState performs all necessary state setup for testing with a
|
||||
// Tailscale Service. When this function returns, the host will be able to
|
||||
// advertise a Service (via [Server.ListenService]) and the client will be able
|
||||
// to dial the Service via the Service name.
|
||||
//
|
||||
// extraSetup, when non-nil, can be used to perform additional state setup and
|
||||
// this state will be observable by client and host when this function returns.
|
||||
func setUpServiceState(t *testing.T, name, ip string, host, client *Server,
|
||||
control *testcontrol.Server, extraSetup func(*testing.T, *testcontrol.Server)) {
|
||||
|
||||
t.Helper()
|
||||
serviceName := tailcfg.ServiceName(name)
|
||||
must.Do(serviceName.Validate())
|
||||
|
||||
// The Service host must have the 'service-host' capability, which
|
||||
// is a mapping from the Service name to the Service VIP.
|
||||
cm := host.lb.NetMap().SelfNode.CapMap()
|
||||
svcIPMap := make(tailcfg.ServiceIPMappings)
|
||||
if cm.Contains(tailcfg.NodeAttrServiceHost) {
|
||||
parsed := must.Get(tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](cm, tailcfg.NodeAttrServiceHost))
|
||||
if len(parsed) != 1 {
|
||||
t.Fatalf("expected only one capability for %v, got %d", tailcfg.NodeAttrServiceHost, len(parsed))
|
||||
}
|
||||
svcIPMap = parsed[0]
|
||||
}
|
||||
svcIPMap[serviceName] = []netip.Addr{netip.MustParseAddr(ip)}
|
||||
svcIPMapJSON := must.Get(json.Marshal(svcIPMap))
|
||||
newCM := cm.AsMap()
|
||||
mak.Set(&newCM, tailcfg.NodeAttrServiceHost, []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)})
|
||||
control.SetNodeCapMap(host.lb.NodeKey(), newCM)
|
||||
|
||||
// The Service host must be allowed to advertise the Service VIP.
|
||||
subnetRoutes := []netip.Prefix{netip.MustParsePrefix(ip + `/32`)}
|
||||
selfAddresses := host.lb.NetMap().SelfNode.Addresses()
|
||||
for _, existingRoute := range host.lb.NetMap().SelfNode.AllowedIPs().All() {
|
||||
if views.SliceContains(selfAddresses, existingRoute) {
|
||||
continue
|
||||
}
|
||||
subnetRoutes = append(subnetRoutes, existingRoute)
|
||||
}
|
||||
control.SetSubnetRoutes(host.lb.NodeKey(), subnetRoutes)
|
||||
|
||||
// The Service host must be a tagged node (any tag will do).
|
||||
serviceHostNode := control.Node(host.lb.NodeKey())
|
||||
serviceHostNode.Tags = append(serviceHostNode.Tags, "some-tag")
|
||||
control.UpdateNode(serviceHostNode)
|
||||
|
||||
// The service client must accept routes advertised by other nodes
|
||||
// (RouteAll is equivalent to --accept-routes).
|
||||
must.Get(client.localClient.EditPrefs(t.Context(), &ipn.MaskedPrefs{
|
||||
RouteAllSet: true,
|
||||
Prefs: ipn.Prefs{
|
||||
RouteAll: true,
|
||||
},
|
||||
}))
|
||||
|
||||
// Do the test's extra setup before configuring DNS. This allows
|
||||
// us to use the configured DNS records as sentinel values when
|
||||
// waiting for all of this setup to be visible to test nodes.
|
||||
if extraSetup != nil {
|
||||
extraSetup(t, control)
|
||||
}
|
||||
|
||||
// Set up DNS for our Service.
|
||||
control.AddDNSRecords(tailcfg.DNSRecord{
|
||||
Name: serviceName.WithoutPrefix() + "." + control.MagicDNSDomain,
|
||||
Value: ip,
|
||||
})
|
||||
|
||||
// Wait until both nodes have up-to-date netmaps before
|
||||
// proceeding with the test.
|
||||
netmapUpToDate := func(nm *netmap.NetworkMap) bool {
|
||||
return nm != nil && slices.ContainsFunc(nm.DNS.ExtraRecords, func(r tailcfg.DNSRecord) bool {
|
||||
return r.Value == ip
|
||||
})
|
||||
}
|
||||
waitForLatestNetmap := func(t *testing.T, s *Server) {
|
||||
t.Helper()
|
||||
w := must.Get(s.localClient.WatchIPNBus(t.Context(), ipn.NotifyInitialNetMap))
|
||||
defer w.Close()
|
||||
for n := must.Get(w.Next()); !netmapUpToDate(n.NetMap); n = must.Get(w.Next()) {
|
||||
}
|
||||
}
|
||||
waitForLatestNetmap(t, client)
|
||||
waitForLatestNetmap(t, host)
|
||||
}
|
||||
|
||||
func TestListenService(t *testing.T) {
|
||||
tstest.Shard(t)
|
||||
|
||||
@@ -1207,81 +1296,19 @@ func TestListenService(t *testing.T) {
|
||||
// We run each test with and without a TUN device ([Server.Tun]).
|
||||
// Note that this TUN device is distinct from TUN mode for Services.
|
||||
doTest := func(t *testing.T, withTUNDevice bool) {
|
||||
ctx := t.Context()
|
||||
|
||||
lt := setupTwoClientTest(t, withTUNDevice)
|
||||
serviceHost := lt.s2
|
||||
serviceClient := lt.s1
|
||||
control := lt.control
|
||||
|
||||
const serviceName = tailcfg.ServiceName("svc:foo")
|
||||
const serviceName = "svc:foo"
|
||||
const serviceVIP = "100.11.22.33"
|
||||
|
||||
// == Set up necessary state in our mock ==
|
||||
setUpServiceState(t, serviceName, serviceVIP,
|
||||
serviceHost, serviceClient, lt.control, tt.extraSetup)
|
||||
|
||||
// The Service host must have the 'service-host' capability, which
|
||||
// is a mapping from the Service name to the Service VIP.
|
||||
cm := serviceHost.lb.NetMap().SelfNode.CapMap().AsMap()
|
||||
mak.Set(&cm, tailcfg.NodeAttrServiceHost, []tailcfg.RawMessage{
|
||||
tailcfg.RawMessage(fmt.Sprintf(`{"%s": ["%s"]}`, serviceName, serviceVIP)),
|
||||
})
|
||||
control.SetNodeCapMap(serviceHost.lb.NodeKey(), cm)
|
||||
|
||||
// The Service host must be allowed to advertise the Service VIP.
|
||||
control.SetSubnetRoutes(serviceHost.lb.NodeKey(), []netip.Prefix{
|
||||
netip.MustParsePrefix(serviceVIP + `/32`),
|
||||
})
|
||||
|
||||
// The Service host must be a tagged node (any tag will do).
|
||||
serviceHostNode := control.Node(serviceHost.lb.NodeKey())
|
||||
serviceHostNode.Tags = append(serviceHostNode.Tags, "some-tag")
|
||||
control.UpdateNode(serviceHostNode)
|
||||
|
||||
// The service client must accept routes advertised by other nodes
|
||||
// (RouteAll is equivalent to --accept-routes).
|
||||
must.Get(serviceClient.localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
RouteAllSet: true,
|
||||
Prefs: ipn.Prefs{
|
||||
RouteAll: true,
|
||||
},
|
||||
}))
|
||||
|
||||
// Do the test's extra setup before configuring DNS. This allows
|
||||
// us to use the configured DNS records as sentinel values when
|
||||
// waiting for all of this setup to be visible to test nodes.
|
||||
if tt.extraSetup != nil {
|
||||
tt.extraSetup(t, control)
|
||||
}
|
||||
|
||||
// Set up DNS for our Service.
|
||||
control.AddDNSRecords(tailcfg.DNSRecord{
|
||||
Name: serviceName.WithoutPrefix() + "." + control.MagicDNSDomain,
|
||||
Value: serviceVIP,
|
||||
})
|
||||
|
||||
// Wait until both nodes have up-to-date netmaps before
|
||||
// proceeding with the test.
|
||||
netmapUpToDate := func(nm *netmap.NetworkMap) bool {
|
||||
return nm != nil && slices.ContainsFunc(nm.DNS.ExtraRecords, func(r tailcfg.DNSRecord) bool {
|
||||
return r.Value == serviceVIP
|
||||
})
|
||||
}
|
||||
waitForLatestNetmap := func(t *testing.T, s *Server) {
|
||||
t.Helper()
|
||||
w := must.Get(s.localClient.WatchIPNBus(t.Context(), ipn.NotifyInitialNetMap))
|
||||
defer w.Close()
|
||||
for n := must.Get(w.Next()); !netmapUpToDate(n.NetMap); n = must.Get(w.Next()) {
|
||||
}
|
||||
}
|
||||
waitForLatestNetmap(t, serviceClient)
|
||||
waitForLatestNetmap(t, serviceHost)
|
||||
|
||||
// == Done setting up mock state ==
|
||||
|
||||
// Start the Service listeners.
|
||||
listeners := make([]*ServiceListener, 0, len(tt.modes))
|
||||
for _, input := range tt.modes {
|
||||
ln := must.Get(serviceHost.ListenService(serviceName.String(), input))
|
||||
ln := must.Get(serviceHost.ListenService(serviceName, input))
|
||||
defer ln.Close()
|
||||
listeners = append(listeners, ln)
|
||||
}
|
||||
@@ -1312,31 +1339,18 @@ func TestListenService(t *testing.T) {
|
||||
t.Run("duplicate_listeners", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
const serviceName = "svc:foo"
|
||||
|
||||
controlURL, control := startControl(t)
|
||||
serviceHost, _, _ := startServer(t, ctx, controlURL, "service-host")
|
||||
serviceClient, _, _ := startServer(t, ctx, controlURL, "service-client")
|
||||
|
||||
// Service hosts must be a tagged node (any tag will do).
|
||||
serviceHostNode := control.Node(serviceHost.lb.NodeKey())
|
||||
serviceHostNode.Tags = append(serviceHostNode.Tags, "some-tag")
|
||||
control.UpdateNode(serviceHostNode)
|
||||
setUpServiceState(t, serviceName, "1.2.3.4", serviceHost, serviceClient, control, nil)
|
||||
|
||||
// Wait for an up-to-date netmap before proceeding with the test.
|
||||
netmapUpToDate := func(nm *netmap.NetworkMap) bool {
|
||||
return nm != nil && nm.SelfNode.IsTagged()
|
||||
}
|
||||
waitForLatestNetmap := func(t *testing.T, s *Server) {
|
||||
t.Helper()
|
||||
w := must.Get(s.localClient.WatchIPNBus(t.Context(), ipn.NotifyInitialNetMap))
|
||||
defer w.Close()
|
||||
for n := must.Get(w.Next()); !netmapUpToDate(n.NetMap); n = must.Get(w.Next()) {
|
||||
}
|
||||
}
|
||||
waitForLatestNetmap(t, serviceHost)
|
||||
|
||||
ln := must.Get(serviceHost.ListenService("svc:foo", ServiceModeTCP{Port: 8080}))
|
||||
ln := must.Get(serviceHost.ListenService(serviceName, ServiceModeTCP{Port: 8080}))
|
||||
defer ln.Close()
|
||||
|
||||
ln, err := serviceHost.ListenService("svc:foo", ServiceModeTCP{Port: 8080})
|
||||
ln, err := serviceHost.ListenService(serviceName, ServiceModeTCP{Port: 8080})
|
||||
if ln != nil {
|
||||
ln.Close()
|
||||
}
|
||||
@@ -1345,7 +1359,7 @@ func TestListenService(t *testing.T) {
|
||||
}
|
||||
|
||||
// An HTTP listener on the same port should also collide
|
||||
ln, err = serviceHost.ListenService("svc:foo", ServiceModeHTTP{Port: 8080})
|
||||
ln, err = serviceHost.ListenService(serviceName, ServiceModeHTTP{Port: 8080})
|
||||
if ln != nil {
|
||||
ln.Close()
|
||||
}
|
||||
@@ -1353,6 +1367,62 @@ func TestListenService(t *testing.T) {
|
||||
t.Fatal("expected error for redundant listener")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple_services", func(t *testing.T) {
|
||||
const numberServices = 10
|
||||
const port = 80
|
||||
|
||||
lt := setupTwoClientTest(t, false)
|
||||
serviceHost := lt.s2
|
||||
serviceClient := lt.s1
|
||||
|
||||
names := make([]string, numberServices)
|
||||
fqdns := make([]string, numberServices)
|
||||
for i := range numberServices {
|
||||
serviceName := "svc:foo" + strconv.Itoa(i+1)
|
||||
serviceIP := `11.22.33.` + strconv.Itoa(i+1)
|
||||
|
||||
setUpServiceState(t, serviceName, serviceIP, serviceHost, serviceClient, lt.control, nil)
|
||||
ln := must.Get(serviceHost.ListenService(serviceName, ServiceModeTCP{Port: port}))
|
||||
defer ln.Close()
|
||||
names[i] = serviceName
|
||||
fqdns[i] = ln.FQDN
|
||||
|
||||
go func() {
|
||||
// Accept a single connection, echo, then return.
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("accept error from %v: %v", serviceName, err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
if _, err := io.Copy(conn, conn); err != nil {
|
||||
t.Errorf("copy error from %v: %v", serviceName, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i := range numberServices {
|
||||
msg := []byte("hello, " + fqdns[i])
|
||||
|
||||
conn := must.Get(serviceClient.Dial(t.Context(), "tcp", fqdns[i]+":"+strconv.Itoa(port)))
|
||||
defer conn.Close()
|
||||
must.Get(conn.Write(msg))
|
||||
buf := make([]byte, len(msg))
|
||||
n := must.Get(conn.Read(buf))
|
||||
if !bytes.Equal(buf[:n], msg) {
|
||||
t.Fatalf("did not receive expected message:\n\tgot: %s\n\twant: %s\n", buf[:n], msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Each of the Services should be advertised by our Service host.
|
||||
advertised := serviceHost.lb.Prefs().AdvertiseServices()
|
||||
for _, name := range names {
|
||||
if !views.SliceContains(advertised, name) {
|
||||
t.Log("advertised Services:", advertised)
|
||||
t.Fatalf("did not find %q in advertised Services", name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestListenServiceClose(t *testing.T) {
|
||||
@@ -1454,28 +1524,14 @@ func TestListenServiceClose(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
const serviceName = "svc:foo"
|
||||
controlURL, control := startControl(t)
|
||||
s, _, _ := startServer(t, ctx, controlURL, "service-host")
|
||||
serviceHost, _, _ := startServer(t, ctx, controlURL, "service-host")
|
||||
serviceClient, _, _ := startServer(t, ctx, controlURL, "service-client")
|
||||
setUpServiceState(t, serviceName, "1.2.3.4", serviceHost, serviceClient, control, nil)
|
||||
|
||||
// Service hosts must be a tagged node (any tag will do).
|
||||
serviceHostNode := control.Node(s.lb.NodeKey())
|
||||
serviceHostNode.Tags = append(serviceHostNode.Tags, "some-tag")
|
||||
control.UpdateNode(serviceHostNode)
|
||||
|
||||
// Wait for an up-to-date netmap before proceeding with the test.
|
||||
netmapUpToDate := func(nm *netmap.NetworkMap) bool {
|
||||
return nm != nil && nm.SelfNode.IsTagged()
|
||||
}
|
||||
waitForLatestNetmap := func(t *testing.T, s *Server) {
|
||||
t.Helper()
|
||||
w := must.Get(s.localClient.WatchIPNBus(t.Context(), ipn.NotifyInitialNetMap))
|
||||
defer w.Close()
|
||||
for n := must.Get(w.Next()); !netmapUpToDate(n.NetMap); n = must.Get(w.Next()) {
|
||||
}
|
||||
}
|
||||
waitForLatestNetmap(t, s)
|
||||
|
||||
tt.run(t, s)
|
||||
tt.run(t, serviceHost)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user