|
|
@@ -14,6 +14,7 @@ import (
|
|
|
"crypto/x509"
|
|
|
"crypto/x509/pkix"
|
|
|
"encoding/json"
|
|
|
+ "encoding/pem"
|
|
|
"errors"
|
|
|
"flag"
|
|
|
"fmt"
|
|
|
@@ -28,6 +29,7 @@ import (
|
|
|
"path/filepath"
|
|
|
"reflect"
|
|
|
"runtime"
|
|
|
+ "slices"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
"sync/atomic"
|
|
|
@@ -38,10 +40,12 @@ import (
|
|
|
dto "github.com/prometheus/client_model/go"
|
|
|
"github.com/prometheus/common/expfmt"
|
|
|
"golang.org/x/net/proxy"
|
|
|
+
|
|
|
"tailscale.com/client/local"
|
|
|
"tailscale.com/cmd/testwrapper/flakytest"
|
|
|
"tailscale.com/internal/client/tailscale"
|
|
|
"tailscale.com/ipn"
|
|
|
+ "tailscale.com/ipn/ipnlocal"
|
|
|
"tailscale.com/ipn/store/mem"
|
|
|
"tailscale.com/net/netns"
|
|
|
"tailscale.com/tailcfg"
|
|
|
@@ -51,6 +55,8 @@ import (
|
|
|
"tailscale.com/tstest/integration/testcontrol"
|
|
|
"tailscale.com/types/key"
|
|
|
"tailscale.com/types/logger"
|
|
|
+ "tailscale.com/types/views"
|
|
|
+ "tailscale.com/util/mak"
|
|
|
"tailscale.com/util/must"
|
|
|
)
|
|
|
|
|
|
@@ -136,7 +142,7 @@ func startControl(t *testing.T) (controlURL string, control *testcontrol.Server)
|
|
|
|
|
|
type testCertIssuer struct {
|
|
|
mu sync.Mutex
|
|
|
- certs map[string]*tls.Certificate
|
|
|
+ certs map[string]ipnlocal.TLSCertKeyPair // keyed by hostname
|
|
|
|
|
|
root *x509.Certificate
|
|
|
rootKey *ecdsa.PrivateKey
|
|
|
@@ -168,18 +174,18 @@ func newCertIssuer() *testCertIssuer {
|
|
|
panic(err)
|
|
|
}
|
|
|
return &testCertIssuer{
|
|
|
- certs: make(map[string]*tls.Certificate),
|
|
|
root: rootCA,
|
|
|
rootKey: rootKey,
|
|
|
+ certs: map[string]ipnlocal.TLSCertKeyPair{},
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (tci *testCertIssuer) getCert(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
|
+func (tci *testCertIssuer) getCert(hostname string) (*ipnlocal.TLSCertKeyPair, error) {
|
|
|
tci.mu.Lock()
|
|
|
defer tci.mu.Unlock()
|
|
|
- cert, ok := tci.certs[chi.ServerName]
|
|
|
+ cert, ok := tci.certs[hostname]
|
|
|
if ok {
|
|
|
- return cert, nil
|
|
|
+ return &cert, nil
|
|
|
}
|
|
|
|
|
|
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
@@ -188,7 +194,7 @@ func (tci *testCertIssuer) getCert(chi *tls.ClientHelloInfo) (*tls.Certificate,
|
|
|
}
|
|
|
certTmpl := &x509.Certificate{
|
|
|
SerialNumber: big.NewInt(1),
|
|
|
- DNSNames: []string{chi.ServerName},
|
|
|
+ DNSNames: []string{hostname},
|
|
|
NotBefore: time.Now(),
|
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
|
}
|
|
|
@@ -196,12 +202,22 @@ func (tci *testCertIssuer) getCert(chi *tls.ClientHelloInfo) (*tls.Certificate,
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
- cert = &tls.Certificate{
|
|
|
- Certificate: [][]byte{certDER, tci.root.Raw},
|
|
|
- PrivateKey: certPrivKey,
|
|
|
+ keyDER, err := x509.MarshalPKCS8PrivateKey(certPrivKey)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
}
|
|
|
- tci.certs[chi.ServerName] = cert
|
|
|
- return cert, nil
|
|
|
+ cert = ipnlocal.TLSCertKeyPair{
|
|
|
+ CertPEM: pem.EncodeToMemory(&pem.Block{
|
|
|
+ Type: "CERTIFICATE",
|
|
|
+ Bytes: certDER,
|
|
|
+ }),
|
|
|
+ KeyPEM: pem.EncodeToMemory(&pem.Block{
|
|
|
+ Type: "PRIVATE KEY",
|
|
|
+ Bytes: keyDER,
|
|
|
+ }),
|
|
|
+ }
|
|
|
+ tci.certs[hostname] = cert
|
|
|
+ return &cert, nil
|
|
|
}
|
|
|
|
|
|
func (tci *testCertIssuer) Pool() *x509.CertPool {
|
|
|
@@ -218,12 +234,11 @@ func startServer(t *testing.T, ctx context.Context, controlURL, hostname string)
|
|
|
tmp := filepath.Join(t.TempDir(), hostname)
|
|
|
os.MkdirAll(tmp, 0755)
|
|
|
s := &Server{
|
|
|
- Dir: tmp,
|
|
|
- ControlURL: controlURL,
|
|
|
- Hostname: hostname,
|
|
|
- Store: new(mem.Store),
|
|
|
- Ephemeral: true,
|
|
|
- getCertForTesting: testCertRoot.getCert,
|
|
|
+ Dir: tmp,
|
|
|
+ ControlURL: controlURL,
|
|
|
+ Hostname: hostname,
|
|
|
+ Store: new(mem.Store),
|
|
|
+ Ephemeral: true,
|
|
|
}
|
|
|
if *verboseNodes {
|
|
|
s.Logf = t.Logf
|
|
|
@@ -234,6 +249,8 @@ func startServer(t *testing.T, ctx context.Context, controlURL, hostname string)
|
|
|
if err != nil {
|
|
|
t.Fatal(err)
|
|
|
}
|
|
|
+ s.lb.ConfigureCertsForTest(testCertRoot.getCert)
|
|
|
+
|
|
|
return s, status.TailscaleIPs[0], status.Self.PublicKey
|
|
|
}
|
|
|
|
|
|
@@ -259,12 +276,11 @@ func TestDialBlocks(t *testing.T) {
|
|
|
tmp := filepath.Join(t.TempDir(), "s2")
|
|
|
os.MkdirAll(tmp, 0755)
|
|
|
s2 := &Server{
|
|
|
- Dir: tmp,
|
|
|
- ControlURL: controlURL,
|
|
|
- Hostname: "s2",
|
|
|
- Store: new(mem.Store),
|
|
|
- Ephemeral: true,
|
|
|
- getCertForTesting: testCertRoot.getCert,
|
|
|
+ Dir: tmp,
|
|
|
+ ControlURL: controlURL,
|
|
|
+ Hostname: "s2",
|
|
|
+ Store: new(mem.Store),
|
|
|
+ Ephemeral: true,
|
|
|
}
|
|
|
if *verboseNodes {
|
|
|
s2.Logf = log.Printf
|
|
|
@@ -842,6 +858,367 @@ func TestFunnelClose(t *testing.T) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+func TestListenService(t *testing.T) {
|
|
|
+ // First test an error case which doesn't require all of the fancy setup.
|
|
|
+ t.Run("untagged_node_error", func(t *testing.T) {
|
|
|
+ ctx := t.Context()
|
|
|
+
|
|
|
+ controlURL, _ := startControl(t)
|
|
|
+ serviceHost, _, _ := startServer(t, ctx, controlURL, "service-host")
|
|
|
+
|
|
|
+ ln, err := serviceHost.ListenService("svc:foo", ServiceModeTCP{Port: 8080})
|
|
|
+ if ln != nil {
|
|
|
+ ln.Close()
|
|
|
+ }
|
|
|
+ if !errors.Is(err, ErrUntaggedServiceHost) {
|
|
|
+ t.Fatalf("expected %v, got %v", ErrUntaggedServiceHost, err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // Now on to the fancier tests.
|
|
|
+
|
|
|
+ type dialFn func(context.Context, string, string) (net.Conn, error)
|
|
|
+
|
|
|
+ // TCP helpers
|
|
|
+ acceptAndEcho := func(t *testing.T, ln net.Listener) {
|
|
|
+ t.Helper()
|
|
|
+ conn, err := ln.Accept()
|
|
|
+ if err != nil {
|
|
|
+ t.Error("accept error:", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer conn.Close()
|
|
|
+ if _, err := io.Copy(conn, conn); err != nil {
|
|
|
+ t.Error("copy error:", err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertEcho := func(t *testing.T, conn net.Conn) {
|
|
|
+ t.Helper()
|
|
|
+ msg := "echo"
|
|
|
+ buf := make([]byte, 1024)
|
|
|
+ if _, err := conn.Write([]byte(msg)); err != nil {
|
|
|
+ t.Fatal("write failed:", err)
|
|
|
+ }
|
|
|
+ n, err := conn.Read(buf)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal("read failed:", err)
|
|
|
+ }
|
|
|
+ got := string(buf[:n])
|
|
|
+ if got != msg {
|
|
|
+ t.Fatalf("unexpected response:\n\twant: %s\n\tgot: %s", msg, got)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // HTTP helpers
|
|
|
+ checkAndEcho := func(t *testing.T, ln net.Listener, check func(r *http.Request)) {
|
|
|
+ t.Helper()
|
|
|
+ if check == nil {
|
|
|
+ check = func(*http.Request) {}
|
|
|
+ }
|
|
|
+ http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ defer r.Body.Close()
|
|
|
+ check(r)
|
|
|
+ if _, err := io.Copy(w, r.Body); err != nil {
|
|
|
+ t.Error("copy error:", err)
|
|
|
+ w.WriteHeader(http.StatusInternalServerError)
|
|
|
+ }
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ assertEchoHTTP := func(t *testing.T, hostname, path string, dial dialFn) {
|
|
|
+ t.Helper()
|
|
|
+ c := http.Client{
|
|
|
+ Transport: &http.Transport{
|
|
|
+ DialContext: dial,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ msg := "echo"
|
|
|
+ resp, err := c.Post("http://"+hostname+path, "text/plain", strings.NewReader(msg))
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal("posting request:", err)
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+ b, err := io.ReadAll(resp.Body)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal("reading body:", err)
|
|
|
+ }
|
|
|
+ got := string(b)
|
|
|
+ if got != msg {
|
|
|
+ t.Fatalf("unexpected response:\n\twant: %s\n\tgot: %s", msg, got)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+
|
|
|
+ // modes is used as input to [Server.ListenService].
|
|
|
+ //
|
|
|
+ // If this slice has multiple modes, then ListenService will be invoked
|
|
|
+ // multiple times. The number of listeners provided to the run function
|
|
|
+ // (below) will always match the number of elements in this slice.
|
|
|
+ modes []ServiceMode
|
|
|
+
|
|
|
+ extraSetup func(t *testing.T, control *testcontrol.Server)
|
|
|
+
|
|
|
+ // run executes the test. This function does not need to close any of
|
|
|
+ // the input resources, but it should close any new resources it opens.
|
|
|
+ // listeners[i] corresponds to inputs[i].
|
|
|
+ run func(t *testing.T, listeners []*ServiceListener, peer *Server)
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "basic_TCP",
|
|
|
+ modes: []ServiceMode{
|
|
|
+ ServiceModeTCP{Port: 99},
|
|
|
+ },
|
|
|
+ run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
|
|
|
+ go acceptAndEcho(t, listeners[0])
|
|
|
+
|
|
|
+ target := fmt.Sprintf("%s:%d", listeners[0].FQDN, 99)
|
|
|
+ conn := must.Get(peer.Dial(t.Context(), "tcp", target))
|
|
|
+ defer conn.Close()
|
|
|
+
|
|
|
+ assertEcho(t, conn)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "TLS_terminated_TCP",
|
|
|
+ modes: []ServiceMode{
|
|
|
+ ServiceModeTCP{
|
|
|
+ TerminateTLS: true,
|
|
|
+ Port: 443,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
|
|
|
+ go acceptAndEcho(t, listeners[0])
|
|
|
+
|
|
|
+ target := fmt.Sprintf("%s:%d", listeners[0].FQDN, 443)
|
|
|
+ conn := must.Get(peer.Dial(t.Context(), "tcp", target))
|
|
|
+ defer conn.Close()
|
|
|
+
|
|
|
+ assertEcho(t, tls.Client(conn, &tls.Config{
|
|
|
+ ServerName: listeners[0].FQDN,
|
|
|
+ RootCAs: testCertRoot.Pool(),
|
|
|
+ }))
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "identity_headers",
|
|
|
+ modes: []ServiceMode{
|
|
|
+ ServiceModeHTTP{
|
|
|
+ Port: 80,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
|
|
|
+ expectHeader := "Tailscale-User-Name"
|
|
|
+ go checkAndEcho(t, listeners[0], func(r *http.Request) {
|
|
|
+ if _, ok := r.Header[expectHeader]; !ok {
|
|
|
+ t.Error("did not see expected header:", expectHeader)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ assertEchoHTTP(t, listeners[0].FQDN, "", peer.Dial)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "identity_headers_TLS",
|
|
|
+ modes: []ServiceMode{
|
|
|
+ ServiceModeHTTP{
|
|
|
+ HTTPS: true,
|
|
|
+ Port: 80,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
|
|
|
+ expectHeader := "Tailscale-User-Name"
|
|
|
+ go checkAndEcho(t, listeners[0], func(r *http.Request) {
|
|
|
+ if _, ok := r.Header[expectHeader]; !ok {
|
|
|
+ t.Error("did not see expected header:", expectHeader)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ dial := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
|
+ tcpConn, err := peer.Dial(ctx, network, addr)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return tls.Client(tcpConn, &tls.Config{
|
|
|
+ ServerName: listeners[0].FQDN,
|
|
|
+ RootCAs: testCertRoot.Pool(),
|
|
|
+ }), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ assertEchoHTTP(t, listeners[0].FQDN, "", dial)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "app_capabilities",
|
|
|
+ modes: []ServiceMode{
|
|
|
+ ServiceModeHTTP{
|
|
|
+ Port: 80,
|
|
|
+ AcceptAppCaps: map[string][]string{
|
|
|
+ "/": {"example.com/cap/all-paths"},
|
|
|
+ "/foo": {"example.com/cap/all-paths", "example.com/cap/foo"},
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ extraSetup: func(t *testing.T, control *testcontrol.Server) {
|
|
|
+ control.SetGlobalAppCaps(tailcfg.PeerCapMap{
|
|
|
+ "example.com/cap/all-paths": []tailcfg.RawMessage{`true`},
|
|
|
+ "example.com/cap/foo": []tailcfg.RawMessage{`true`},
|
|
|
+ })
|
|
|
+ },
|
|
|
+ run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
|
|
|
+ allPathsCap := "example.com/cap/all-paths"
|
|
|
+ fooCap := "example.com/cap/foo"
|
|
|
+ checkCaps := func(r *http.Request) {
|
|
|
+ rawCaps, ok := r.Header["Tailscale-App-Capabilities"]
|
|
|
+ if !ok {
|
|
|
+ t.Error("no app capabilities header")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if len(rawCaps) != 1 {
|
|
|
+ t.Error("expected one app capabilities header value, got", len(rawCaps))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var caps map[string][]any
|
|
|
+ if err := json.Unmarshal([]byte(rawCaps[0]), &caps); err != nil {
|
|
|
+ t.Error("error unmarshaling app caps:", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if _, ok := caps[allPathsCap]; !ok {
|
|
|
+ t.Errorf("got app caps, but %v is not present; saw:\n%v", allPathsCap, caps)
|
|
|
+ }
|
|
|
+ if strings.HasPrefix(r.URL.Path, "/foo") {
|
|
|
+ if _, ok := caps[fooCap]; !ok {
|
|
|
+ t.Errorf("%v should be present for /foo request; saw:\n%v", fooCap, caps)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if _, ok := caps[fooCap]; ok {
|
|
|
+ t.Errorf("%v should not be present for non-/foo request; saw:\n%v", fooCap, caps)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ go checkAndEcho(t, listeners[0], checkCaps)
|
|
|
+ assertEchoHTTP(t, listeners[0].FQDN, "", peer.Dial)
|
|
|
+ assertEchoHTTP(t, listeners[0].FQDN, "/foo", peer.Dial)
|
|
|
+ assertEchoHTTP(t, listeners[0].FQDN, "/foo/bar", peer.Dial)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "multiple_ports",
|
|
|
+ modes: []ServiceMode{
|
|
|
+ ServiceModeTCP{
|
|
|
+ Port: 99,
|
|
|
+ },
|
|
|
+ ServiceModeHTTP{
|
|
|
+ Port: 80,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
|
|
|
+ go acceptAndEcho(t, listeners[0])
|
|
|
+
|
|
|
+ target := fmt.Sprintf("%s:%d", listeners[0].FQDN, 99)
|
|
|
+ conn := must.Get(peer.Dial(t.Context(), "tcp", target))
|
|
|
+ defer conn.Close()
|
|
|
+ assertEcho(t, conn)
|
|
|
+
|
|
|
+ go checkAndEcho(t, listeners[1], nil)
|
|
|
+ assertEchoHTTP(t, listeners[1].FQDN, "", peer.Dial)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ // Overview:
|
|
|
+ // - start test control
|
|
|
+ // - start 2 tsnet nodes:
|
|
|
+ // one to act as Service host and a second to act as a peer client
|
|
|
+ // - configure necessary state on control mock
|
|
|
+ // - start a Service listener from the host
|
|
|
+ // - call tt.run with our test bed
|
|
|
+ //
|
|
|
+ // This ends up also testing the Service forwarding logic in
|
|
|
+ // LocalBackend, but that's useful too.
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ ctx := t.Context()
|
|
|
+
|
|
|
+ controlURL, control := startControl(t)
|
|
|
+ serviceHost, _, _ := startServer(t, ctx, controlURL, "service-host")
|
|
|
+ serviceClient, _, _ := startServer(t, ctx, controlURL, "service-client")
|
|
|
+
|
|
|
+ const serviceName = tailcfg.ServiceName("svc:foo")
|
|
|
+ const serviceVIP = "100.11.22.33"
|
|
|
+
|
|
|
+ // == Set up necessary state in our mock ==
|
|
|
+
|
|
|
+ // The Service host must have the 'service-host' capability, which
|
|
|
+ // is a mapping from the Service name to the Service VIP.
|
|
|
+ var serviceHostCaps map[tailcfg.ServiceName]views.Slice[netip.Addr]
|
|
|
+ mak.Set(&serviceHostCaps, serviceName, views.SliceOf([]netip.Addr{netip.MustParseAddr(serviceVIP)}))
|
|
|
+ j := must.Get(json.Marshal(serviceHostCaps))
|
|
|
+ cm := serviceHost.lb.NetMap().SelfNode.CapMap().AsMap()
|
|
|
+ mak.Set(&cm, tailcfg.NodeAttrServiceHost, []tailcfg.RawMessage{tailcfg.RawMessage(j)})
|
|
|
+ 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,
|
|
|
+ },
|
|
|
+ }))
|
|
|
+
|
|
|
+ // Set up DNS for our Service.
|
|
|
+ control.AddDNSRecords(tailcfg.DNSRecord{
|
|
|
+ Name: serviceName.WithoutPrefix() + "." + control.MagicDNSDomain,
|
|
|
+ Value: serviceVIP,
|
|
|
+ })
|
|
|
+
|
|
|
+ if tt.extraSetup != nil {
|
|
|
+ tt.extraSetup(t, control)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Force netmap updates to avoid race conditions. The nodes need to
|
|
|
+ // see our control updates before we can start the test.
|
|
|
+ must.Do(control.ForceNetmapUpdate(ctx, serviceHost.lb.NodeKey()))
|
|
|
+ must.Do(control.ForceNetmapUpdate(ctx, serviceClient.lb.NodeKey()))
|
|
|
+ netmapUpToDate := func(s *Server) bool {
|
|
|
+ nm := s.lb.NetMap()
|
|
|
+ return slices.ContainsFunc(nm.DNS.ExtraRecords, func(r tailcfg.DNSRecord) bool {
|
|
|
+ return r.Value == serviceVIP
|
|
|
+ })
|
|
|
+ }
|
|
|
+ for !netmapUpToDate(serviceClient) {
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
+ }
|
|
|
+ for !netmapUpToDate(serviceHost) {
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
+ }
|
|
|
+
|
|
|
+ // == 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))
|
|
|
+ defer ln.Close()
|
|
|
+ listeners = append(listeners, ln)
|
|
|
+ }
|
|
|
+
|
|
|
+ tt.run(t, listeners, serviceClient)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func TestListenerClose(t *testing.T) {
|
|
|
tstest.Shard(t)
|
|
|
ctx := context.Background()
|