@@ -52,6 +52,7 @@ import (
5252 "tailscale.com/net/proxymux"
5353 "tailscale.com/net/socks5"
5454 "tailscale.com/net/tsdial"
55+ "tailscale.com/tailcfg"
5556 "tailscale.com/tsd"
5657 "tailscale.com/types/bools"
5758 "tailscale.com/types/logger"
@@ -158,8 +159,6 @@ type Server struct {
158159 // that the control server will allow the node to adopt that tag.
159160 AdvertiseTags []string
160161
161- getCertForTesting func (* tls.ClientHelloInfo ) (* tls.Certificate , error )
162-
163162 initOnce sync.Once
164163 initErr error
165164 lb * ipnlocal.LocalBackend
@@ -1101,9 +1100,6 @@ func (s *Server) RegisterFallbackTCPHandler(cb FallbackTCPHandler) func() {
11011100// It calls GetCertificate on the localClient, passing in the ClientHelloInfo.
11021101// For testing, if s.getCertForTesting is set, it will call that instead.
11031102func (s * Server ) getCert (hi * tls.ClientHelloInfo ) (* tls.Certificate , error ) {
1104- if s .getCertForTesting != nil {
1105- return s .getCertForTesting (hi )
1106- }
11071103 lc , err := s .LocalClient ()
11081104 if err != nil {
11091105 return nil , err
@@ -1239,6 +1235,189 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
12391235 return tls .NewListener (ln , tlsConfig ), nil
12401236}
12411237
1238+ // TODO: doc
1239+ type ServiceOption interface {
1240+ serviceOption ()
1241+ }
1242+
1243+ type serviceOptionTerminateTLS struct {}
1244+
1245+ func (serviceOptionTerminateTLS ) serviceOption () {}
1246+
1247+ // TODO: doc
1248+ func ServiceOptionTerminateTLS () ServiceOption {
1249+ return serviceOptionTerminateTLS {}
1250+ }
1251+
1252+ type serviceOptionPROXYProtocol struct {
1253+ version int
1254+ }
1255+
1256+ func (serviceOptionPROXYProtocol ) serviceOption () {}
1257+
1258+ // TODO: doc
1259+ func ServiceOptionPROXYProtocol (version int ) ServiceOption {
1260+ return serviceOptionPROXYProtocol {version }
1261+ }
1262+
1263+ type serviceOptionAppCapabilities struct {
1264+ path string
1265+ caps []string
1266+ }
1267+
1268+ func (serviceOptionAppCapabilities ) serviceOption () {}
1269+
1270+ // TODO: doc
1271+ func ServiceOptionAppCapabilities (capabilities ... string ) ServiceOption {
1272+ return ServiceOptionAppCapabilitiesForPath ("/" , capabilities ... )
1273+ }
1274+
1275+ // TODO: doc
1276+ func ServiceOptionAppCapabilitiesForPath (path string , capabilities ... string ) ServiceOption {
1277+ return serviceOptionAppCapabilities {path , capabilities }
1278+ }
1279+
1280+ type serviceOptionWithHeaders struct {}
1281+
1282+ func (serviceOptionWithHeaders ) serviceOption () {}
1283+
1284+ // TODO: doc
1285+ func ServiceOptionWithHeaders () ServiceOption {
1286+ return serviceOptionWithHeaders {}
1287+ }
1288+
1289+ // ErrUntaggedServiceHost is returned by ListenService when run on a node
1290+ // without any ACL tags. A node must use a tag-based identity to act as a
1291+ // Service host. For more information, see:
1292+ // https://tailscale.com/kb/1552/tailscale-services#prerequisites
1293+ var ErrUntaggedServiceHost = errors .New ("service hosts must be tagged nodes" )
1294+
1295+ // TODO: doc
1296+ // TODO: tailcfg.ServiceName?
1297+ func (s * Server ) ListenService (name string , port uint16 , opts ... ServiceOption ) (net.Listener , error ) {
1298+ if err := tailcfg .ServiceName (name ).Validate (); err != nil {
1299+ return nil , err
1300+ }
1301+ svcName := name
1302+
1303+ // TODO:
1304+ // - create example for a Service with multiple ports
1305+ // - support web handlers?
1306+ // - at least app capabilities need to work
1307+ // - everything else could serve directly or use http.ReverseProxy, right?
1308+ // - maybe worth an http.ReverseProxy example?
1309+ // - support TUN mode
1310+
1311+ // Process options.
1312+ terminateTLS := false
1313+ proxyProtocol := 0
1314+ capsMap := map [string ][]tailcfg.PeerCapability {} // mount point => caps
1315+ isHTTP := false
1316+ for _ , o := range opts {
1317+ switch opt := o .(type ) {
1318+ case serviceOptionTerminateTLS :
1319+ terminateTLS = true
1320+ case serviceOptionPROXYProtocol :
1321+ proxyProtocol = opt .version
1322+ case serviceOptionWithHeaders :
1323+ isHTTP = true
1324+ case serviceOptionAppCapabilities :
1325+ isHTTP = true
1326+ caps := make ([]tailcfg.PeerCapability , 0 , len (opt .caps ))
1327+ for _ , c := range opt .caps {
1328+ caps = append (caps , tailcfg .PeerCapability (c ))
1329+ }
1330+ capsMap [opt .path ] = append (capsMap [opt .path ], caps ... )
1331+ default :
1332+ return nil , fmt .Errorf ("unknown opts FunnelOption type %T" , o )
1333+ }
1334+ }
1335+
1336+ ctx := context .Background ()
1337+ _ , err := s .Up (ctx )
1338+ if err != nil {
1339+ return nil , err
1340+ }
1341+
1342+ lc := s .localClient
1343+
1344+ st , err := lc .StatusWithoutPeers (ctx )
1345+ if err != nil {
1346+ return nil , fmt .Errorf ("fetching ACL tags: %w" , err )
1347+ }
1348+ if st .Self .Tags == nil || st .Self .Tags .Len () == 0 {
1349+ return nil , ErrUntaggedServiceHost
1350+ }
1351+
1352+ prefs , err := lc .GetPrefs (ctx )
1353+ if err != nil {
1354+ return nil , fmt .Errorf ("fetching node preferences: %w" , err )
1355+ }
1356+ if ! slices .Contains (prefs .AdvertiseServices , svcName ) {
1357+ // TODO: do we need to undo this edit on error?
1358+ _ , err = lc .EditPrefs (ctx , & ipn.MaskedPrefs {
1359+ AdvertiseServicesSet : true ,
1360+ Prefs : ipn.Prefs {
1361+ AdvertiseServices : append (prefs .AdvertiseServices , svcName ),
1362+ },
1363+ })
1364+ if err != nil {
1365+ return nil , fmt .Errorf ("updating advertised Services: %w" , err )
1366+ }
1367+ }
1368+
1369+ srvConfig , err := lc .GetServeConfig (ctx )
1370+ if err != nil {
1371+ return nil , fmt .Errorf ("fetching node serve config: %w" , err )
1372+ }
1373+ if srvConfig == nil {
1374+ srvConfig = new (ipn.ServeConfig )
1375+ }
1376+
1377+ // Start listening on a TCP socket.
1378+ ln , err := net .Listen ("tcp" , "localhost:0" )
1379+ if err != nil {
1380+ return nil , fmt .Errorf ("starting local listener: %w" , err )
1381+ }
1382+
1383+ if isHTTP {
1384+ useTLS := false // TODO: set correctly
1385+ mds := st .CurrentTailnet .MagicDNSSuffix
1386+ setHandler := func (h ipn.HTTPHandler , path string ) {
1387+ // TODO: do we need to add the path to the end of the proxy value?
1388+ h .Proxy = ln .Addr ().String ()
1389+ srvConfig .SetWebHandler (& h , svcName , port , path , useTLS , mds )
1390+ }
1391+ // Set a web handler for every mount point in the caps map. If we don't
1392+ // end up with a root handler after that, we need to set one.
1393+ haveRootHandler := false
1394+ for path , caps := range capsMap {
1395+ if path == "/" {
1396+ haveRootHandler = true
1397+ }
1398+ setHandler (ipn.HTTPHandler {AcceptAppCaps : caps }, path )
1399+ }
1400+ if ! haveRootHandler {
1401+ setHandler (ipn.HTTPHandler {}, "/" )
1402+ }
1403+ } else {
1404+ // Forward all connections from service-hostname:port to our socket.
1405+ srvConfig .SetTCPForwardingForService (
1406+ port , ln .Addr ().String (), tailcfg .ServiceName (svcName ),
1407+ terminateTLS , proxyProtocol , st .CurrentTailnet .MagicDNSSuffix )
1408+ }
1409+
1410+ if err := lc .SetServeConfig (ctx , srvConfig ); err != nil {
1411+ ln .Close ()
1412+ return nil , err
1413+ }
1414+
1415+ // TODO: wrap returned listener such that Close stops advertising the
1416+ // Service (should update prefs, serve config, etc.)
1417+
1418+ return ln , nil
1419+ }
1420+
12421421type listenOn string
12431422
12441423const (
0 commit comments