diff --git a/ipn/ipnlocal/cert.go b/ipn/ipnlocal/cert.go index 16ba57004..51b00cb18 100644 --- a/ipn/ipnlocal/cert.go +++ b/ipn/ipnlocal/cert.go @@ -314,9 +314,9 @@ func (b *LocalBackend) getCertStore() (certStore, error) { // certificate registration. // // Certificates will be served based on the subject name or subject alternative -// names (SANs) in the certificate. If this backend should serve certificates +// names (SANs) in the certificates. If this backend should serve certificates // for hostnames like foo.tail-scale.ts.net or test-service.tail-scale.ts.net, -// then those names need to appear in the subject name or SAN. +// then those names need to appear as a subject name or SAN. func (b *LocalBackend) SetCertsForTest(certs ...TLSCertKeyPair) { testenv.AssertInTest() m := map[string]TLSCertKeyPair{} diff --git a/tsnet/example/tsnet-services/tsnet-services.go b/tsnet/example/tsnet-services/tsnet-services.go index 23b129671..da0e1b0b2 100644 --- a/tsnet/example/tsnet-services/tsnet-services.go +++ b/tsnet/example/tsnet-services/tsnet-services.go @@ -50,6 +50,7 @@ func main() { } defer ln.Close() + // TODO: provide access to FQDN from listener and use that instead fmt.Printf("Listening on https://%v\n", tailcfg.AsServiceName(*svcName).WithoutPrefix()) err = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go index 2a67b2ff6..deeae617a 100644 --- a/tsnet/tsnet_test.go +++ b/tsnet/tsnet_test.go @@ -14,6 +14,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/json" + "encoding/pem" "errors" "flag" "fmt" @@ -37,10 +38,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" @@ -747,11 +750,15 @@ func TestFunnel(t *testing.T) { func TestListenService(t *testing.T) { tests := []struct { name string - opts ServiceOption + opts []ServiceOption }{ { name: "basic_TCP_service", }, + { + name: "TLS_terminated_TCP", + opts: []ServiceOption{ServiceOptionTerminateTLS()}, + }, // TODO: // Success cases: // - TLS-terminated-TCP @@ -771,6 +778,9 @@ func TestListenService(t *testing.T) { // - start a Service listener from host // - dial Service from peer client // - try to have a conversation + // + // 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() @@ -779,9 +789,11 @@ func TestListenService(t *testing.T) { serviceClient, _, _ := startServer(t, ctx, controlURL, "service-client") const serviceName = tailcfg.ServiceName("svc:foo") - const servicePort uint16 = 80 + const servicePort uint16 = 99 const serviceVIP = "100.11.22.33" + serviceFQDN := serviceName.WithoutPrefix() + "." + control.MagicDNSDomain + // == Set up necessary state in our mock == // The Service host must have the 'service-host' capability, which @@ -803,6 +815,23 @@ func TestListenService(t *testing.T) { serviceHostNode.Tags = append(serviceHostNode.Tags, "some-tag") control.UpdateNode(serviceHostNode) + // Configure a certificate for the Service domain (in production, + // the local backend would use an ACME client to obtain a cert). + // This is only used when serving over TLS. + cert := must.Get(testCertRoot.getCert(&tls.ClientHelloInfo{ + ServerName: serviceFQDN, + })) + serviceHost.lb.SetCertsForTest(ipnlocal.TLSCertKeyPair{ + CertPEM: pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Certificate[0], + }), + KeyPEM: pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: must.Get(x509.MarshalPKCS8PrivateKey(cert.PrivateKey)), + }), + }) + // The service client must accept routes advertised by other nodes // (RouteAll is equivalent to --accept-routes). must.Get(serviceClient.localClient.EditPrefs(ctx, &ipn.MaskedPrefs{ @@ -815,7 +844,7 @@ func TestListenService(t *testing.T) { // == Done setting up mock state == // Start a Service listener. - ln := must.Get(serviceHost.ListenService(serviceName.String(), servicePort)) + ln := must.Get(serviceHost.ListenService(serviceName.String(), servicePort, tt.opts...)) defer ln.Close() // Accept the first connection on ln and echo back what we receive. @@ -835,6 +864,15 @@ func TestListenService(t *testing.T) { conn := must.Get(serviceClient.Dial(ctx, "tcp", target)) defer conn.Close() + for _, opt := range tt.opts { + if _, ok := opt.(serviceOptionTerminateTLS); ok { + conn = tls.Client(conn, &tls.Config{ + ServerName: serviceFQDN, + RootCAs: testCertRoot.Pool(), + }) + } + } + msg := "hello, Service" buf := make([]byte, 1024) if _, err := conn.Write([]byte(msg)); err != nil { diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index 19964c91f..2f23384bd 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -838,6 +838,9 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key. CapMap: capMap, Capabilities: slices.Collect(maps.Keys(capMap)), } + if s.MagicDNSDomain != "" { + node.Name = node.Name + "." + s.MagicDNSDomain + } s.nodes[nk] = node } requireAuth := s.RequireAuth @@ -1261,9 +1264,7 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, dns := s.DNSConfig if dns != nil && s.MagicDNSDomain != "" { dns = dns.Clone() - dns.CertDomains = []string{ - node.Hostinfo.Hostname() + "." + s.MagicDNSDomain, - } + dns.CertDomains = append(dns.CertDomains, node.Hostinfo.Hostname()+"."+s.MagicDNSDomain) } res = &tailcfg.MapResponse{