Merge 2244badf0d806e5869b54fdd55cbb40e381ee8e3 into 14db99241f4f4191776ec22b4ff02f4563087b34

This commit is contained in:
KevinLiang10 2025-03-24 20:36:36 +00:00 committed by GitHub
commit e1cbef00fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1220 additions and 177 deletions

View File

@ -47,6 +47,25 @@ func runAdvertise(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
if len(services) > 0 {
fmt.Println("Advertising this node as new destination for services:", services)
fmt.Println("This node will accept connection for services once the services are configured locally and approved on the admin console.")
sc, err := localClient.GetServeConfig(ctx)
if err != nil {
return fmt.Errorf("failed to get serve config: %w", err)
}
notServedServices := make([]string, 0)
for _, svc := range services {
if _, ok := sc.Services[tailcfg.ServiceName(svc)]; !ok {
notServedServices = append(notServedServices, svc)
}
}
if len(notServedServices) > 0 {
fmt.Println("The following services are not configured to be served yet: ", strings.Join(notServedServices, ", "))
fmt.Println("To configure services, run tailscale serve --service=\"<svc:dns-label>\" for each service.")
fmt.Printf("eg. tailscale serve --service=%q 3000\n", notServedServices[0])
}
}
_, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{ _, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
AdvertiseServicesSet: true, AdvertiseServicesSet: true,

View File

@ -141,6 +141,7 @@ type localServeClient interface {
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error)
IncrementCounter(ctx context.Context, name string, delta int) error IncrementCounter(ctx context.Context, name string, delta int) error
GetPrefs(ctx context.Context) (*ipn.Prefs, error)
} }
// serveEnv is the environment the serve command runs within. All I/O should be // serveEnv is the environment the serve command runs within. All I/O should be
@ -154,7 +155,7 @@ type serveEnv struct {
json bool // output JSON (status only for now) json bool // output JSON (status only for now)
// v2 specific flags // v2 specific flags
bg bool // background mode bg bgBoolFlag // background mode
setPath string // serve path setPath string // serve path
https uint // HTTP port https uint // HTTP port
http uint // HTTP port http uint // HTTP port
@ -162,6 +163,8 @@ type serveEnv struct {
tlsTerminatedTCP uint // a TLS terminated TCP port tlsTerminatedTCP uint // a TLS terminated TCP port
subcmd serveMode // subcommand subcmd serveMode // subcommand
yes bool // update without prompt yes bool // update without prompt
service string // service name
tun bool // redirect traffic to OS for service
lc localServeClient // localClient interface, specific to serve lc localServeClient // localClient interface, specific to serve
@ -354,12 +357,16 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
if err != nil { if err != nil {
return err return err
} }
if sc.IsTCPForwardingOnPort(srvPort) { if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
fmt.Fprintf(Stderr, "error: cannot serve web; already serving TCP\n") fmt.Fprintf(Stderr, "error: cannot serve web; already serving TCP\n")
return errHelp return errHelp
} }
sc.SetWebHandler(h, dnsName, srvPort, mount, useTLS) st, err := e.getLocalClientStatusWithoutPeers(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
sc.SetWebHandler(st, h, dnsName, srvPort, mount, useTLS)
if !reflect.DeepEqual(cursc, sc) { if !reflect.DeepEqual(cursc, sc) {
if err := e.lc.SetServeConfig(ctx, sc); err != nil { if err := e.lc.SetServeConfig(ctx, sc); err != nil {
@ -411,11 +418,11 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mou
if err != nil { if err != nil {
return err return err
} }
if sc.IsTCPForwardingOnPort(srvPort) { if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
return errors.New("cannot remove web handler; currently serving TCP") return errors.New("cannot remove web handler; currently serving TCP")
} }
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort)))) hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
if !sc.WebHandlerExists(hp, mount) { if !sc.WebHandlerExists(dnsName, hp, mount) {
return errors.New("error: handler does not exist") return errors.New("error: handler does not exist")
} }
sc.RemoveWebHandler(dnsName, srvPort, []string{mount}, false) sc.RemoveWebHandler(dnsName, srvPort, []string{mount}, false)
@ -550,7 +557,7 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
fwdAddr := "127.0.0.1:" + dstPortStr fwdAddr := "127.0.0.1:" + dstPortStr
if sc.IsServingWeb(srcPort) { if sc.IsServingWeb(srcPort, "") {
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort) return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
} }
@ -581,11 +588,15 @@ func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
if sc == nil { if sc == nil {
sc = new(ipn.ServeConfig) sc = new(ipn.ServeConfig)
} }
if sc.IsServingWeb(src) { dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
if sc.IsServingWeb(src, dnsName) {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src) return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
} }
if ph := sc.GetTCPPortHandler(src); ph != nil { if ph := sc.GetTCPPortHandler(src, dnsName); ph != nil {
sc.RemoveTCPForwarding(src) sc.RemoveTCPForwarding(dnsName, src)
return e.lc.SetServeConfig(ctx, sc) return e.lc.SetServeConfig(ctx, sc)
} }
return errors.New("error: serve config does not exist") return errors.New("error: serve config does not exist")
@ -682,7 +693,7 @@ func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) erro
} }
scheme := "https" scheme := "https"
if sc.IsServingHTTP(port) { if sc.IsServingHTTP(port, host) {
scheme = "http" scheme = "http"
} }

View File

@ -877,6 +877,10 @@ var fakeStatus = &ipnstate.Status{
}, },
} }
var fakePref = &ipn.Prefs{
AdvertiseServices: []string{"svc:test"},
}
func (lc *fakeLocalServeClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) { func (lc *fakeLocalServeClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return fakeStatus, nil return fakeStatus, nil
} }
@ -891,6 +895,10 @@ func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.
return nil return nil
} }
func (lc *fakeLocalServeClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
return fakePref, nil
}
type mockQueryFeatureResponse struct { type mockQueryFeatureResponse struct {
resp *tailcfg.QueryFeatureResponse resp *tailcfg.QueryFeatureResponse
err error err error

View File

@ -18,6 +18,7 @@ import (
"os/signal" "os/signal"
"path" "path"
"path/filepath" "path/filepath"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -40,6 +41,32 @@ type commandInfo struct {
LongHelp string LongHelp string
} }
type bgBoolFlag struct {
Value bool
SetByUser bool // tracks if the flag was set by the user
}
// Set sets the boolean flag and wether it's explicitly set by user based on the string value.
func (b *bgBoolFlag) Set(s string) error {
if s == "true" {
b.Value = true
} else if s == "false" {
b.Value = false
} else {
return fmt.Errorf("invalid boolean value: %s", s)
}
b.SetByUser = true
return nil
}
// This is a hack to make the flag package recognize that this is a boolean flag.
func (b *bgBoolFlag) IsBoolFlag() bool { return true }
// String returns the string representation of the boolean flag.
func (b *bgBoolFlag) String() string {
return fmt.Sprintf("%t", b.Value)
}
var serveHelpCommon = strings.TrimSpace(` var serveHelpCommon = strings.TrimSpace(`
<target> can be a file, directory, text, or most commonly the location to a service running on the <target> can be a file, directory, text, or most commonly the location to a service running on the
local machine. The location to the location service can be expressed as a port number (e.g., 3000), local machine. The location to the location service can be expressed as a port number (e.g., 3000),
@ -72,6 +99,7 @@ const (
serveTypeHTTP serveTypeHTTP
serveTypeTCP serveTypeTCP
serveTypeTLSTerminatedTCP serveTypeTLSTerminatedTCP
serveTypeTun
) )
var infoMap = map[serveMode]commandInfo{ var infoMap = map[serveMode]commandInfo{
@ -119,7 +147,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
Exec: e.runServeCombined(subcmd), Exec: e.runServeCombined(subcmd),
FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) { FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) {
fs.BoolVar(&e.bg, "bg", false, "Run the command as a background process (default false)") fs.Var(&e.bg, "bg", "Run the command as a background process (default false)")
fs.StringVar(&e.setPath, "set-path", "", "Appends the specified path to the base URL for accessing the underlying service") fs.StringVar(&e.setPath, "set-path", "", "Appends the specified path to the base URL for accessing the underlying service")
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)") fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
if subcmd == serve { if subcmd == serve {
@ -127,7 +155,9 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
} }
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port") fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port") fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
fs.StringVar(&e.service, "service", "", "Name of the service to serve.")
fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts (default false)") fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts (default false)")
fs.BoolVar(&e.tun, "tun", false, "Forward all traffic to the local machine (default false), only supported for services")
}), }),
UsageFunc: usageFuncNoDefaultValues, UsageFunc: usageFuncNoDefaultValues,
Subcommands: []*ffcli.Command{ Subcommands: []*ffcli.Command{
@ -161,9 +191,16 @@ func (e *serveEnv) validateArgs(subcmd serveMode, args []string) error {
fmt.Fprint(e.stderr(), "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n") fmt.Fprint(e.stderr(), "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n")
return errHelpFunc(subcmd) return errHelpFunc(subcmd)
} }
if len(args) == 0 && e.tun {
return nil
}
if len(args) == 0 { if len(args) == 0 {
return flag.ErrHelp return flag.ErrHelp
} }
if e.tun && len(args) > 1 {
fmt.Fprintln(e.stderr(), "Error: invalid argument format")
return errHelpFunc(subcmd)
}
if len(args) > 2 { if len(args) > 2 {
fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args)) fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args))
return errHelpFunc(subcmd) return errHelpFunc(subcmd)
@ -182,6 +219,9 @@ func (e *serveEnv) validateArgs(subcmd serveMode, args []string) error {
// runServeCombined is the entry point for the "tailscale {serve,funnel}" commands. // runServeCombined is the entry point for the "tailscale {serve,funnel}" commands.
func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
e.subcmd = subcmd e.subcmd = subcmd
if !e.bg.SetByUser {
e.bg.Value = e.service != ""
}
return func(ctx context.Context, args []string) error { return func(ctx context.Context, args []string) error {
// Undocumented debug command (not using ffcli subcommands) to set raw // Undocumented debug command (not using ffcli subcommands) to set raw
@ -197,15 +237,17 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
} }
return e.lc.SetServeConfig(ctx, sc) return e.lc.SetServeConfig(ctx, sc)
} }
if err := e.validateArgs(subcmd, args); err != nil { if err := e.validateArgs(subcmd, args); err != nil {
return err return err
} }
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel() defer cancel()
funnel := subcmd == funnel funnel := subcmd == funnel
if e.service != "" && funnel {
return errors.New("Error: --service flag is not supported with funnel")
}
if funnel { if funnel {
// verify node has funnel capabilities // verify node has funnel capabilities
if err := e.verifyFunnelEnabled(ctx, 443); err != nil { if err := e.verifyFunnelEnabled(ctx, 443); err != nil {
@ -213,12 +255,16 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
} }
} }
if e.service != "" && e.bg.SetByUser && !e.bg.Value {
return errors.New("Error: --service flag is only compatible with background mode")
}
mount, err := cleanURLPath(e.setPath) mount, err := cleanURLPath(e.setPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to clean the mount point: %w", err) return fmt.Errorf("failed to clean the mount point: %w", err)
} }
srvType, srvPort, err := srvTypeAndPortFromFlags(e) srvType, srvPort, wasDefaultServe, err := srvTypeAndPortFromFlags(e)
if err != nil { if err != nil {
fmt.Fprintf(e.stderr(), "error: %v\n\n", err) fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
return errHelpFunc(subcmd) return errHelpFunc(subcmd)
@ -229,6 +275,11 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
return fmt.Errorf("error getting serve config: %w", err) return fmt.Errorf("error getting serve config: %w", err)
} }
prefs, err := e.lc.GetPrefs(ctx)
if err != nil {
return fmt.Errorf("error getting prefs: %w", err)
}
// nil if no config // nil if no config
if sc == nil { if sc == nil {
sc = new(ipn.ServeConfig) sc = new(ipn.ServeConfig)
@ -245,7 +296,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
// foreground or background. // foreground or background.
parentSC := sc parentSC := sc
turnOff := "off" == args[len(args)-1] turnOff := len(args) > 0 && "off" == args[len(args)-1]
if !turnOff && srvType == serveTypeHTTPS { if !turnOff && srvType == serveTypeHTTPS {
// Running serve with https requires that the tailnet has enabled // Running serve with https requires that the tailnet has enabled
// https cert provisioning. Send users through an interactive flow // https cert provisioning. Send users through an interactive flow
@ -262,10 +313,21 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
} }
var watcher *tailscale.IPNBusWatcher var watcher *tailscale.IPNBusWatcher
wantFg := !e.bg && !turnOff forService := e.service != ""
if forService {
err = tailcfg.ServiceName(e.service).Validate()
if err != nil {
return fmt.Errorf("failed to parse service name: %w", err)
}
dnsName = e.service
}
if !forService && srvType == serveTypeTun {
return errors.New("tun mode is only supported for services")
}
wantFg := !forService && !e.bg.Value && !turnOff
if wantFg { if wantFg {
// validate the config before creating a WatchIPNBus session // validate the config before creating a WatchIPNBus session
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil { if err := e.validateConfig(parentSC, srvPort, srvType, dnsName); err != nil {
return err return err
} }
@ -291,13 +353,21 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
var msg string var msg string
if turnOff { if turnOff {
err = e.unsetServe(sc, dnsName, srvType, srvPort, mount) if wasDefaultServe && forService {
delete(sc.Services, tailcfg.ServiceName(dnsName))
} else { } else {
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil { err = e.unsetServe(sc, st, dnsName, srvType, srvPort, mount)
}
} else {
if err := e.validateConfig(parentSC, srvPort, srvType, dnsName); err != nil {
return err return err
} }
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel) target := ""
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort) if len(args) > 0 {
target = args[0]
}
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, target, funnel)
msg = e.messageForPort(sc, st, prefs, dnsName, srvType, srvPort)
} }
if err != nil { if err != nil {
fmt.Fprintf(e.stderr(), "error: %v\n\n", err) fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
@ -333,7 +403,27 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
const backgroundExistsMsg = "background configuration already exists, use `tailscale %s --%s=%d off` to remove the existing configuration" const backgroundExistsMsg = "background configuration already exists, use `tailscale %s --%s=%d off` to remove the existing configuration"
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType) error { // validateConfig checks if the serve config is valid to serve the type wanted on the port.
// dnsName is a FQDN or a serviceName (with `svc:` prefix).
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType, dnsName string) error {
forService := ipn.IsServiceName(dnsName)
var tcpHandlerForPort *ipn.TCPPortHandler
if forService {
svc := sc.FindServiceConfig(tailcfg.ServiceName(dnsName))
if svc == nil {
return nil
}
if wantServe == serveTypeTun && (svc.TCP != nil || svc.Web != nil) {
return errors.New("service already has a TCP or Web handler, cannot serve in TUN mode")
}
if svc.Tun && wantServe != serveTypeTun {
return errors.New("service is already being served in TUN mode")
}
if svc.TCP[port] == nil {
return nil
}
tcpHandlerForPort = svc.TCP[port]
} else {
sc, isFg := sc.FindConfig(port) sc, isFg := sc.FindConfig(port)
if sc == nil { if sc == nil {
return nil return nil
@ -341,12 +431,14 @@ func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe se
if isFg { if isFg {
return errors.New("foreground already exists under this port") return errors.New("foreground already exists under this port")
} }
if !e.bg { if !e.bg.Value {
return fmt.Errorf(backgroundExistsMsg, infoMap[e.subcmd].Name, wantServe.String(), port) return fmt.Errorf(backgroundExistsMsg, infoMap[e.subcmd].Name, wantServe.String(), port)
} }
existingServe := serveFromPortHandler(sc.TCP[port]) tcpHandlerForPort = sc.TCP[port]
}
existingServe := serveFromPortHandler(tcpHandlerForPort)
if wantServe != existingServe { if wantServe != existingServe {
return fmt.Errorf("want %q but port is already serving %q", wantServe, existingServe) return fmt.Errorf("want to serve %q but port is already serving %q for %q", wantServe, existingServe, dnsName)
} }
return nil return nil
} }
@ -371,7 +463,7 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
switch srvType { switch srvType {
case serveTypeHTTPS, serveTypeHTTP: case serveTypeHTTPS, serveTypeHTTP:
useTLS := srvType == serveTypeHTTPS useTLS := srvType == serveTypeHTTPS
err := e.applyWebServe(sc, dnsName, srvPort, useTLS, mount, target) err := e.applyWebServe(sc, st, dnsName, srvPort, useTLS, mount, target)
if err != nil { if err != nil {
return fmt.Errorf("failed apply web serve: %w", err) return fmt.Errorf("failed apply web serve: %w", err)
} }
@ -379,45 +471,59 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
if e.setPath != "" { if e.setPath != "" {
return fmt.Errorf("cannot mount a path for TCP serve") return fmt.Errorf("cannot mount a path for TCP serve")
} }
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target) err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
if err != nil { if err != nil {
return fmt.Errorf("failed to apply TCP serve: %w", err) return fmt.Errorf("failed to apply TCP serve: %w", err)
} }
case serveTypeTun:
svcName := tailcfg.ServiceName(dnsName)
if _, ok := sc.Services[svcName]; !ok {
mak.Set(&sc.Services, svcName, new(ipn.ServiceConfig))
}
sc.Services[svcName].Tun = true
default: default:
return fmt.Errorf("invalid type %q", srvType) return fmt.Errorf("invalid type %q", srvType)
} }
// update the serve config based on if funnel is enabled // update the serve config based on if funnel is enabled
// Since funnel is not supported for services, we only apply it for node's serve.
if !ipn.IsServiceName(dnsName) {
e.applyFunnel(sc, dnsName, srvPort, allowFunnel) e.applyFunnel(sc, dnsName, srvPort, allowFunnel)
}
return nil return nil
} }
var ( var (
msgFunnelAvailable = "Available on the internet:" msgFunnelAvailable = "Available on the internet:"
msgServeAvailable = "Available within your tailnet:" msgServeAvailable = "Available within your tailnet:"
msgServiceIPNotAssigned = "This service doesn't have VIPs assigned yet, once VIP is assigned, it will be available in your Tailnet as:"
msgRunningInBackground = "%s started and running in the background." msgRunningInBackground = "%s started and running in the background."
msgRunningTunServie = "IPv4 and IPv6 traffic to %s is being routed to your operating system."
msgDisableProxy = "To disable the proxy, run: tailscale %s --%s=%d off" msgDisableProxy = "To disable the proxy, run: tailscale %s --%s=%d off"
msgDisableServiceProxy = "To disable the proxy, run: tailscale serve --service=%s --%s=%d off"
msgDisableServiceTun = "To disable the service in TUN mode, run: tailscale serve --service=%s --tun off"
msgDisableService = "To disable the service entirely, run: tailscale serve --service=%s off"
msgServiceNotAdvertised = "This service is not advertised on this node yet, use `tailscale advertise --services=svc:%s` to advertise it."
msgToExit = "Press Ctrl+C to exit." msgToExit = "Press Ctrl+C to exit."
) )
// messageForPort returns a message for the given port based on the // messageForPort returns a message for the given port based on the
// serve config and status. // serve config and status.
func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16) string { func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, prefs *ipn.Prefs, dnsName string, srvType serveType, srvPort uint16) string {
var output strings.Builder var output strings.Builder
forService := ipn.IsServiceName(dnsName)
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort)))) var hp ipn.HostPort
var webConfig *ipn.WebServerConfig
if sc.AllowFunnel[hp] == true { var tcpHandler *ipn.TCPPortHandler
output.WriteString(msgFunnelAvailable) ips := st.TailscaleIPs
} else { host := dnsName
output.WriteString(msgServeAvailable) if forService {
host = tailcfg.ServiceName(dnsName).WithoutPrefix() + "." + st.CurrentTailnet.MagicDNSSuffix
} }
output.WriteString("\n\n") hp = ipn.HostPort(net.JoinHostPort(host, strconv.Itoa(int(srvPort))))
scheme := "https" scheme := "https"
if sc.IsServingHTTP(srvPort) { if sc.IsServingHTTP(srvPort, dnsName) {
scheme = "http" scheme = "http"
} }
@ -438,37 +544,70 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
} }
return "", "" return "", ""
} }
if forService {
svcName := tailcfg.ServiceName(dnsName)
serviceIPMaps, err := tailcfg.UnmarshalNodeCapJSON[tailcfg.ServiceIPMappings](st.Self.CapMap, tailcfg.NodeAttrServiceHost)
if err != nil || len(serviceIPMaps) == 0 || serviceIPMaps[0][svcName] == nil {
output.WriteString(msgServiceIPNotAssigned)
ips = nil
} else {
output.WriteString(msgServeAvailable)
ips = serviceIPMaps[0][svcName]
}
output.WriteString("\n\n")
svc := sc.FindServiceConfig(svcName)
if srvType == serveTypeTun && svc.Tun {
output.WriteString(fmt.Sprintf(msgRunningTunServie, host))
output.WriteString("\n")
output.WriteString(fmt.Sprintf(msgDisableServiceTun, dnsName))
output.WriteString("\n")
output.WriteString(fmt.Sprintf(msgDisableService, dnsName))
return output.String()
}
if svc != nil {
webConfig = svc.Web[hp]
tcpHandler = svc.TCP[srvPort]
}
} else {
if sc.AllowFunnel[hp] == true {
output.WriteString(msgFunnelAvailable)
} else {
output.WriteString(msgServeAvailable)
}
output.WriteString("\n\n")
webConfig = sc.Web[hp]
tcpHandler = sc.TCP[srvPort]
}
if sc.Web[hp] != nil { if webConfig != nil {
mounts := slicesx.MapKeys(sc.Web[hp].Handlers) mounts := slicesx.MapKeys(webConfig.Handlers)
sort.Slice(mounts, func(i, j int) bool { sort.Slice(mounts, func(i, j int) bool {
return len(mounts[i]) < len(mounts[j]) return len(mounts[i]) < len(mounts[j])
}) })
for _, m := range mounts { for _, m := range mounts {
h := sc.Web[hp].Handlers[m] h := webConfig.Handlers[m]
t, d := srvTypeAndDesc(h) t, d := srvTypeAndDesc(h)
output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, dnsName, portPart, m)) output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, host, portPart, m))
output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d)) output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d))
} }
} else if sc.TCP[srvPort] != nil { } else if tcpHandler != nil {
h := sc.TCP[srvPort] h := tcpHandler
tlsStatus := "TLS over TCP" tlsStatus := "TLS over TCP"
if h.TerminateTLS != "" { if h.TerminateTLS != "" {
tlsStatus = "TLS terminated" tlsStatus = "TLS terminated"
} }
output.WriteString(fmt.Sprintf("%s://%s%s\n", scheme, dnsName, portPart))
output.WriteString(fmt.Sprintf("|-- tcp://%s (%s)\n", hp, tlsStatus)) output.WriteString(fmt.Sprintf("|-- tcp://%s (%s)\n", hp, tlsStatus))
for _, a := range st.TailscaleIPs { for _, a := range ips {
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort))) ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort)))
output.WriteString(fmt.Sprintf("|-- tcp://%s\n", ipp)) output.WriteString(fmt.Sprintf("|-- tcp://%s\n", ipp))
} }
output.WriteString(fmt.Sprintf("|--> tcp://%s\n", h.TCPForward)) output.WriteString(fmt.Sprintf("|--> tcp://%s\n\n", h.TCPForward))
} }
if !e.bg { if !forService && !e.bg.Value {
output.WriteString(msgToExit) output.WriteString(msgToExit)
return output.String() return output.String()
} }
@ -478,14 +617,23 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
output.WriteString(fmt.Sprintf(msgRunningInBackground, subCmdUpper)) output.WriteString(fmt.Sprintf(msgRunningInBackground, subCmdUpper))
output.WriteString("\n") output.WriteString("\n")
if forService {
if !slices.Contains(prefs.AdvertiseServices, dnsName) {
output.WriteString(fmt.Sprintf(msgServiceNotAdvertised, dnsName))
output.WriteString("\n")
}
output.WriteString(fmt.Sprintf(msgDisableServiceProxy, dnsName, srvType.String(), srvPort))
output.WriteString("\n")
output.WriteString(fmt.Sprintf(msgDisableService, dnsName))
} else {
output.WriteString(fmt.Sprintf(msgDisableProxy, subCmd, srvType.String(), srvPort)) output.WriteString(fmt.Sprintf(msgDisableProxy, subCmd, srvType.String(), srvPort))
}
return output.String() return output.String()
} }
func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target string) error { func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16, useTLS bool, mount, target string) error {
h := new(ipn.HTTPHandler) h := new(ipn.HTTPHandler)
switch { switch {
case strings.HasPrefix(target, "text:"): case strings.HasPrefix(target, "text:"):
text := strings.TrimPrefix(target, "text:") text := strings.TrimPrefix(target, "text:")
@ -521,11 +669,11 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
} }
// TODO: validation needs to check nested foreground configs // TODO: validation needs to check nested foreground configs
if sc.IsTCPForwardingOnPort(srvPort) { if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
return errors.New("cannot serve web; already serving TCP") return errors.New("cannot serve web; already serving TCP")
} }
sc.SetWebHandler(h, dnsName, srvPort, mount, useTLS) sc.SetWebHandler(st, h, dnsName, srvPort, mount, useTLS)
return nil return nil
} }
@ -552,7 +700,7 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
} }
// TODO: needs to account for multiple configs from foreground mode // TODO: needs to account for multiple configs from foreground mode
if sc.IsServingWeb(srcPort) { if sc.IsServingWeb(srcPort, dnsName) {
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort) return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
} }
@ -577,18 +725,24 @@ func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint
} }
// unsetServe removes the serve config for the given serve port. // unsetServe removes the serve config for the given serve port.
func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string) error { // dnsName is a FQDN or a serviceName (with `svc:` prefix).
func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string) error {
switch srvType { switch srvType {
case serveTypeHTTPS, serveTypeHTTP: case serveTypeHTTPS, serveTypeHTTP:
err := e.removeWebServe(sc, dnsName, srvPort, mount) err := e.removeWebServe(sc, st, dnsName, srvPort, mount)
if err != nil { if err != nil {
return fmt.Errorf("failed to remove web serve: %w", err) return fmt.Errorf("failed to remove web serve: %w", err)
} }
case serveTypeTCP, serveTypeTLSTerminatedTCP: case serveTypeTCP, serveTypeTLSTerminatedTCP:
err := e.removeTCPServe(sc, srvPort) err := e.removeTCPServe(sc, dnsName, srvPort)
if err != nil { if err != nil {
return fmt.Errorf("failed to remove TCP serve: %w", err) return fmt.Errorf("failed to remove TCP serve: %w", err)
} }
case serveTypeTun:
err := e.removeTunServe(sc, dnsName)
if err != nil {
return fmt.Errorf("failed to remove TUN serve: %w", err)
}
default: default:
return fmt.Errorf("invalid type %q", srvType) return fmt.Errorf("invalid type %q", srvType)
} }
@ -598,7 +752,7 @@ func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serve
return nil return nil
} }
func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, err error) { func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, wasDefault bool, err error) {
sourceMap := map[serveType]uint{ sourceMap := map[serveType]uint{
serveTypeHTTP: e.http, serveTypeHTTP: e.http,
serveTypeHTTPS: e.https, serveTypeHTTPS: e.https,
@ -611,22 +765,30 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, er
for k, v := range sourceMap { for k, v := range sourceMap {
if v != 0 { if v != 0 {
if v > math.MaxUint16 { if v > math.MaxUint16 {
return 0, 0, fmt.Errorf("port number %d is too high for %s flag", v, srvType) return 0, 0, false, fmt.Errorf("port number %d is too high for %s flag", v, srvType)
} }
srcTypeCount++ srcTypeCount++
srvType = k srvType = k
srvPort = uint16(v) srvPort = uint16(v)
wasDefault = false
} }
} }
if e.tun {
srcTypeCount++
srvType = serveTypeTun
wasDefault = false
}
if srcTypeCount > 1 { if srcTypeCount > 1 {
return 0, 0, fmt.Errorf("cannot serve multiple types for a single mount point") return 0, 0, false, fmt.Errorf("cannot serve multiple types for a single mount point")
} else if srcTypeCount == 0 { } else if srcTypeCount == 0 {
srvType = serveTypeHTTPS srvType = serveTypeHTTPS
srvPort = 443 srvPort = 443
wasDefault = true
} }
return srvType, srvPort, nil return srvType, srvPort, wasDefault, nil
} }
// isLegacyInvocation helps transition customers who have been using the beta // isLegacyInvocation helps transition customers who have been using the beta
@ -727,27 +889,45 @@ func isLegacyInvocation(subcmd serveMode, args []string) (string, bool) {
// and removes funnel if no remaining mounts exist for the serve port. // and removes funnel if no remaining mounts exist for the serve port.
// The srvPort argument is the serving port and the mount argument is // The srvPort argument is the serving port and the mount argument is
// the mount point or registered path to remove. // the mount point or registered path to remove.
func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, mount string) error { func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16, mount string) error {
if sc.IsTCPForwardingOnPort(srvPort) { if sc == nil {
return errors.New("cannot remove web handler; currently serving TCP") return nil
}
forService := ipn.IsServiceName(dnsName)
portStr := strconv.Itoa(int(srvPort))
var hp ipn.HostPort
var webServeMap map[ipn.HostPort]*ipn.WebServerConfig
if forService {
svcName := tailcfg.ServiceName(dnsName)
dnsNameForService := svcName.WithoutPrefix() + "." + st.CurrentTailnet.MagicDNSSuffix
hp = ipn.HostPort(net.JoinHostPort(dnsNameForService, portStr))
if svc, ok := sc.Services[svcName]; !ok || svc == nil {
return errors.New("error: service does not exist")
} else {
webServeMap = svc.Web
}
} else {
hp = ipn.HostPort(net.JoinHostPort(dnsName, portStr))
webServeMap = sc.Web
} }
portStr := strconv.Itoa(int(srvPort)) if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
hp := ipn.HostPort(net.JoinHostPort(dnsName, portStr)) return errors.New("cannot remove web handler; currently serving TCP")
}
var targetExists bool var targetExists bool
var mounts []string var mounts []string
// mount is deduced from e.setPath but it is ambiguous as // mount is deduced from e.setPath but it is ambiguous as
// to whether the user explicitly passed "/" or it was defaulted to. // to whether the user explicitly passed "/" or it was defaulted to.
if e.setPath == "" { if e.setPath == "" {
targetExists = sc.Web[hp] != nil && len(sc.Web[hp].Handlers) > 0 targetExists = webServeMap[hp] != nil && len(webServeMap[hp].Handlers) > 0
if targetExists { if targetExists {
for mount := range sc.Web[hp].Handlers { for mount := range webServeMap[hp].Handlers {
mounts = append(mounts, mount) mounts = append(mounts, mount)
} }
} }
} else { } else {
targetExists = sc.WebHandlerExists(hp, mount) targetExists = sc.WebHandlerExists(dnsName, hp, mount)
mounts = []string{mount} mounts = []string{mount}
} }
@ -756,29 +936,49 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
} }
if len(mounts) > 1 { if len(mounts) > 1 {
msg := fmt.Sprintf("Are you sure you want to delete %d handlers under port %s?", len(mounts), portStr) msg := fmt.Sprintf("Are you sure you want to delete %d handlers under port %s for %q?", len(mounts), portStr, dnsName)
if !e.yes && !promptYesNo(msg) { if !e.yes && !promptYesNo(msg) {
return nil return nil
} }
} }
if forService {
sc.RemoveServiceWebHandler(st, tailcfg.ServiceName(dnsName), srvPort, mounts)
} else {
sc.RemoveWebHandler(dnsName, srvPort, mounts, true) sc.RemoveWebHandler(dnsName, srvPort, mounts, true)
}
return nil return nil
} }
// removeTCPServe removes the TCP forwarding configuration for the // removeTCPServe removes the TCP forwarding configuration for the
// given srvPort, or serving port. // given srvPort, or serving port for the given dnsName.
func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error { func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, dnsName string, src uint16) error {
if sc == nil { if sc == nil {
return nil return nil
} }
if sc.GetTCPPortHandler(src) == nil { if sc.GetTCPPortHandler(src, dnsName) == nil {
return errors.New("error: serve config does not exist") return errors.New("error: serve config does not exist")
} }
if sc.IsServingWeb(src) { if sc.IsServingWeb(src, dnsName) {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src) return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
} }
sc.RemoveTCPForwarding(src) sc.RemoveTCPForwarding(dnsName, src)
return nil
}
func (e *serveEnv) removeTunServe(sc *ipn.ServeConfig, dnsName string) error {
if sc == nil {
return nil
}
svcName := tailcfg.ServiceName(dnsName)
svc, ok := sc.Services[svcName]
if !ok || svc == nil {
return errors.New("error: service does not exist")
}
if !svc.Tun {
return errors.New("error: service is not being served in TUN mode")
}
delete(sc.Services, svcName)
return nil return nil
} }

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/netip"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -19,6 +20,7 @@ import (
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
) )
func TestServeDevConfigMutations(t *testing.T) { func TestServeDevConfigMutations(t *testing.T) {
@ -874,15 +876,17 @@ func TestValidateConfig(t *testing.T) {
name string name string
desc string desc string
cfg *ipn.ServeConfig cfg *ipn.ServeConfig
dns string
servePort uint16 servePort uint16
serveType serveType serveType serveType
bg bool bg bgBoolFlag
wantErr bool wantErr bool
}{ }{
{ {
name: "nil_config", name: "nil_config",
desc: "when config is nil, all requests valid", desc: "when config is nil, all requests valid",
cfg: nil, cfg: nil,
dns: "node.test.ts.net",
servePort: 3000, servePort: 3000,
serveType: serveTypeHTTPS, serveType: serveTypeHTTPS,
}, },
@ -894,7 +898,8 @@ func TestValidateConfig(t *testing.T) {
443: {HTTPS: true}, 443: {HTTPS: true},
}, },
}, },
bg: true, dns: "node.test.ts.net",
bg: bgBoolFlag{true, false},
servePort: 10000, servePort: 10000,
serveType: serveTypeHTTPS, serveType: serveTypeHTTPS,
}, },
@ -906,7 +911,8 @@ func TestValidateConfig(t *testing.T) {
443: {TCPForward: "http://localhost:4545"}, 443: {TCPForward: "http://localhost:4545"},
}, },
}, },
bg: true, dns: "node.test.ts.net",
bg: bgBoolFlag{true, false},
servePort: 443, servePort: 443,
serveType: serveTypeTCP, serveType: serveTypeTCP,
}, },
@ -918,7 +924,8 @@ func TestValidateConfig(t *testing.T) {
443: {HTTPS: true}, 443: {HTTPS: true},
}, },
}, },
bg: true, dns: "node.test.ts.net",
bg: bgBoolFlag{true, false},
servePort: 443, servePort: 443,
serveType: serveTypeHTTP, serveType: serveTypeHTTP,
wantErr: true, wantErr: true,
@ -938,6 +945,7 @@ func TestValidateConfig(t *testing.T) {
}, },
}, },
}, },
dns: "node.test.ts.net",
servePort: 4040, servePort: 4040,
serveType: serveTypeTCP, serveType: serveTypeTCP,
}, },
@ -953,16 +961,95 @@ func TestValidateConfig(t *testing.T) {
}, },
}, },
}, },
dns: "node.test.ts.net",
servePort: 3000, servePort: 3000,
serveType: serveTypeTCP, serveType: serveTypeTCP,
wantErr: true, wantErr: true,
}, },
{
name: "new_service_tcp",
desc: "no error when adding a new service port",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
},
},
},
dns: "svc:foo",
servePort: 8080,
serveType: serveTypeTCP,
},
{
name: "override_service_tcp",
desc: "no error when overwriting a previous service port",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "http://localhost:4545"},
},
},
},
},
dns: "svc:foo",
servePort: 443,
serveType: serveTypeTCP,
},
{
name: "override_service_tcp",
desc: "error when overwriting a previous service port with a different serve type",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
},
},
},
dns: "svc:foo",
servePort: 443,
serveType: serveTypeHTTP,
wantErr: true,
},
{
name: "override_service_tcp",
desc: "error when setting previous tcp service to tun mode",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "http://localhost:4545"},
},
},
},
},
dns: "svc:foo",
serveType: serveTypeTun,
wantErr: true,
},
{
name: "override_service_tun",
desc: "error when setting previous tun service to tcp forwarder",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
Tun: true,
},
},
},
dns: "svc:foo",
serveType: serveTypeTCP,
servePort: 443,
wantErr: true,
},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
se := serveEnv{bg: tc.bg} se := serveEnv{bg: tc.bg}
err := se.validateConfig(tc.cfg, tc.servePort, tc.serveType) err := se.validateConfig(tc.cfg, tc.servePort, tc.serveType, tc.dns)
if err == nil && tc.wantErr { if err == nil && tc.wantErr {
t.Fatal("expected an error but got nil") t.Fatal("expected an error but got nil")
} }
@ -981,6 +1068,7 @@ func TestSrcTypeFromFlags(t *testing.T) {
expectedType serveType expectedType serveType
expectedPort uint16 expectedPort uint16
expectedErr bool expectedErr bool
expectedWasDefault bool
}{ }{
{ {
name: "only http set", name: "only http set",
@ -988,6 +1076,7 @@ func TestSrcTypeFromFlags(t *testing.T) {
expectedType: serveTypeHTTP, expectedType: serveTypeHTTP,
expectedPort: 80, expectedPort: 80,
expectedErr: false, expectedErr: false,
expectedWasDefault: false,
}, },
{ {
name: "only https set", name: "only https set",
@ -995,6 +1084,7 @@ func TestSrcTypeFromFlags(t *testing.T) {
expectedType: serveTypeHTTPS, expectedType: serveTypeHTTPS,
expectedPort: 10000, expectedPort: 10000,
expectedErr: false, expectedErr: false,
expectedWasDefault: false,
}, },
{ {
name: "only tcp set", name: "only tcp set",
@ -1002,6 +1092,7 @@ func TestSrcTypeFromFlags(t *testing.T) {
expectedType: serveTypeTCP, expectedType: serveTypeTCP,
expectedPort: 8000, expectedPort: 8000,
expectedErr: false, expectedErr: false,
expectedWasDefault: false,
}, },
{ {
name: "only tls-terminated-tcp set", name: "only tls-terminated-tcp set",
@ -1009,6 +1100,7 @@ func TestSrcTypeFromFlags(t *testing.T) {
expectedType: serveTypeTLSTerminatedTCP, expectedType: serveTypeTLSTerminatedTCP,
expectedPort: 8080, expectedPort: 8080,
expectedErr: false, expectedErr: false,
expectedWasDefault: false,
}, },
{ {
name: "defaults to https, port 443", name: "defaults to https, port 443",
@ -1016,18 +1108,20 @@ func TestSrcTypeFromFlags(t *testing.T) {
expectedType: serveTypeHTTPS, expectedType: serveTypeHTTPS,
expectedPort: 443, expectedPort: 443,
expectedErr: false, expectedErr: false,
expectedWasDefault: true,
}, },
{ {
name: "multiple types set", name: "multiple types set",
env: &serveEnv{http: 80, https: 443}, env: &serveEnv{http: 80, https: 443},
expectedPort: 0, expectedPort: 0,
expectedErr: true, expectedErr: true,
expectedWasDefault: false,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
srcType, srcPort, err := srvTypeAndPortFromFlags(tt.env) srcType, srcPort, explicitSet, err := srvTypeAndPortFromFlags(tt.env)
if (err != nil) != tt.expectedErr { if (err != nil) != tt.expectedErr {
t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err) t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err)
} }
@ -1037,6 +1131,9 @@ func TestSrcTypeFromFlags(t *testing.T) {
if srcPort != tt.expectedPort { if srcPort != tt.expectedPort {
t.Errorf("Expected srcPort: %d, got: %d", tt.expectedPort, srcPort) t.Errorf("Expected srcPort: %d, got: %d", tt.expectedPort, srcPort)
} }
if explicitSet != tt.expectedWasDefault {
t.Errorf("Expected defaultFlag: %v, got: %v", tt.expectedWasDefault, explicitSet)
}
}) })
} }
} }
@ -1076,11 +1173,21 @@ func TestCleanURLPath(t *testing.T) {
} }
func TestMessageForPort(t *testing.T) { func TestMessageForPort(t *testing.T) {
svcIPMap := tailcfg.ServiceIPMappings{
"svc:foo": []netip.Addr{
netip.MustParseAddr("100.101.101.101"),
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:6565:6565"),
},
}
svcIPMapJSON, _ := json.Marshal(svcIPMap)
svcIPMapJSONRawMSG := tailcfg.RawMessage(svcIPMapJSON)
tests := []struct { tests := []struct {
name string name string
subcmd serveMode subcmd serveMode
serveConfig *ipn.ServeConfig serveConfig *ipn.ServeConfig
status *ipnstate.Status status *ipnstate.Status
prefs *ipn.Prefs
dnsName string dnsName string
srvType serveType srvType serveType
srvPort uint16 srvPort uint16
@ -1147,13 +1254,252 @@ func TestMessageForPort(t *testing.T) {
fmt.Sprintf(msgDisableProxy, "serve", "http", 80), fmt.Sprintf(msgDisableProxy, "serve", "http", 80),
}, "\n"), }, "\n"),
}, },
{
name: "serve service http",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{
AdvertiseServices: []string{"svc:foo"},
},
dnsName: "svc:foo",
srvType: serveTypeHTTP,
srvPort: 80,
expected: strings.Join([]string{
msgServeAvailable,
"",
"http://foo.test.ts.net/",
"|-- proxy http://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "http", 80),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service no capmap",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{
AdvertiseServices: []string{"svc:bar"},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
expected: strings.Join([]string{
msgServiceIPNotAssigned,
"",
"http://bar.test.ts.net/",
"|-- proxy http://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:bar", "http", 80),
fmt.Sprintf(msgDisableService, "svc:bar"),
}, "\n"),
},
{
name: "serve unadvertised service http",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: ipn.NewPrefs(),
dnsName: "svc:foo",
srvType: serveTypeHTTP,
srvPort: 80,
expected: strings.Join([]string{
msgServeAvailable,
"",
"http://foo.test.ts.net/",
"|-- proxy http://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgServiceNotAdvertised, "svc:foo"),
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "http", 80),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service https non-default port",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
2200: {HTTPS: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:2200": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
dnsName: "svc:foo",
srvType: serveTypeHTTPS,
srvPort: 2200,
expected: strings.Join([]string{
msgServeAvailable,
"",
"https://foo.test.ts.net:2200/",
"|-- proxy http://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "https", 2200),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service TCPForward",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
2200: {TCPForward: "localhost:3000"},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
dnsName: "svc:foo",
srvType: serveTypeTCP,
srvPort: 2200,
expected: strings.Join([]string{
msgServeAvailable,
"",
"|-- tcp://foo.test.ts.net:2200 (TLS over TCP)",
"|-- tcp://100.101.101.101:2200",
"|-- tcp://[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:2200",
"|--> tcp://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "tcp", 2200),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service Tun",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
Tun: true,
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
dnsName: "svc:foo",
srvType: serveTypeTun,
expected: strings.Join([]string{
msgServeAvailable,
"",
fmt.Sprintf(msgRunningTunServie, "foo.test.ts.net"),
fmt.Sprintf(msgDisableServiceTun, "svc:foo"),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
} }
for _, tt := range tests { for _, tt := range tests {
e := &serveEnv{bg: true, subcmd: tt.subcmd} e := &serveEnv{bg: bgBoolFlag{true, false}, subcmd: tt.subcmd}
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
actual := e.messageForPort(tt.serveConfig, tt.status, tt.dnsName, tt.srvType, tt.srvPort) actual := e.messageForPort(tt.serveConfig, tt.status, tt.prefs, tt.dnsName, tt.srvType, tt.srvPort)
if actual == "" { if actual == "" {
t.Errorf("Got empty message") t.Errorf("Got empty message")
@ -1277,6 +1623,219 @@ func TestIsLegacyInvocation(t *testing.T) {
} }
} }
func TestSetServe(t *testing.T) {
e := &serveEnv{}
tests := []struct {
name string
desc string
cfg *ipn.ServeConfig
st *ipnstate.Status
dnsName string
srvType serveType
srvPort uint16
mountPath string
target string
allowFunnel bool
expected *ipn.ServeConfig
expectErr bool
}{
{
name: "add new handler",
desc: "add a new http handler to empty config",
cfg: &ipn.ServeConfig{},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3000",
expected: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
{
name: "update http handler",
desc: "update an existing http handler on the same port to same type",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3001",
expected: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3001"},
},
},
},
},
},
{
name: "update TCP handler",
desc: "update an existing TCP handler on the same port to a http handler",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "http://localhost:3000"}},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3001",
expectErr: true,
},
{
name: "add new service handler",
desc: "add a new service TCP handler to empty config",
cfg: &ipn.ServeConfig{},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeTCP,
srvPort: 80,
target: "3000",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
},
},
},
},
{
name: "update service handler",
desc: "update an existing service TCP handler on the same port to same type",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeTCP,
srvPort: 80,
target: "3001",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3001"}},
},
},
},
},
{
name: "update service handler",
desc: "update an existing service TCP handler on the same port to a http handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3001",
expectErr: true,
},
{
name: "add new service handler",
desc: "add a new service HTTP handler to empty config",
cfg: &ipn.ServeConfig{},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3000",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
},
{
name: "add new service handler",
desc: "add a new service handler in tun mode to empty config",
cfg: &ipn.ServeConfig{},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeTun,
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
Tun: true,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := e.setServe(tt.cfg, tt.st, tt.dnsName, tt.srvType, tt.srvPort, tt.mountPath, tt.target, tt.allowFunnel)
if err != nil && !tt.expectErr {
t.Fatalf("got error: %v; did not expect error.", err)
}
if err == nil && tt.expectErr {
t.Fatalf("got no error; expected error.")
}
if !tt.expectErr && !reflect.DeepEqual(tt.cfg, tt.expected) {
t.Logf("got: %v", tt.cfg.Services["svc:bar"].TCP[80])
t.Fatalf("got: %v; expected: %v", tt.cfg, tt.expected)
}
})
}
}
// exactErrMsg returns an error checker that wants exactly the provided want error. // exactErrMsg returns an error checker that wants exactly the provided want error.
// If optName is non-empty, it's used in the error message. // If optName is non-empty, it's used in the error message.
func exactErrMsg(want error) func(error) string { func exactErrMsg(want error) func(error) string {

View File

@ -260,7 +260,7 @@ func printFunnelStatus(ctx context.Context) {
} }
sni, portStr, _ := net.SplitHostPort(string(hp)) sni, portStr, _ := net.SplitHostPort(string(hp))
p, _ := strconv.ParseUint(portStr, 10, 16) p, _ := strconv.ParseUint(portStr, 10, 16)
isTCP := sc.IsTCPForwardingOnPort(uint16(p)) isTCP := sc.IsTCPForwardingOnPort(uint16(p), sni)
url := "https://" url := "https://"
if isTCP { if isTCP {
url = "tcp://" url = "tcp://"

View File

@ -264,7 +264,7 @@ func serveOnLocalTailscaled(ctx context.Context, lc *local.Client, st *ipnstate.
fmt.Printf("setting funnel for %s:%v\n", serverURL, dstPort) fmt.Printf("setting funnel for %s:%v\n", serverURL, dstPort)
foregroundSc.SetFunnel(serverURL, dstPort, shouldFunnel) foregroundSc.SetFunnel(serverURL, dstPort, shouldFunnel)
foregroundSc.SetWebHandler(&ipn.HTTPHandler{ foregroundSc.SetWebHandler(st, &ipn.HTTPHandler{
Proxy: fmt.Sprintf("https://%s", net.JoinHostPort(serverURL, strconv.Itoa(int(dstPort)))), Proxy: fmt.Sprintf("https://%s", net.JoinHostPort(serverURL, strconv.Itoa(int(dstPort)))),
}, serverURL, uint16(*flagPort), "/", true) }, serverURL, uint16(*flagPort), "/", true)
err = lc.SetServeConfig(ctx, sc) err = lc.SetServeConfig(ctx, sc)

View File

@ -166,15 +166,26 @@ type HTTPHandler struct {
// WebHandlerExists reports whether if the ServeConfig Web handler exists for // WebHandlerExists reports whether if the ServeConfig Web handler exists for
// the given host:port and mount point. // the given host:port and mount point.
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool { func (sc *ServeConfig) WebHandlerExists(dnsName string, hp HostPort, mount string) bool {
h := sc.GetWebHandler(hp, mount) h := sc.GetWebHandler(dnsName, hp, mount)
return h != nil return h != nil
} }
// GetWebHandler returns the HTTPHandler for the given host:port and mount point. // GetWebHandler returns the HTTPHandler for the given host:port and mount point.
// Returns nil if the handler does not exist. // Returns nil if the handler does not exist.
func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler { func (sc *ServeConfig) GetWebHandler(dnsName string, hp HostPort, mount string) *HTTPHandler {
if sc == nil || sc.Web[hp] == nil { if sc == nil {
return nil
}
if IsServiceName(dnsName) {
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc.Web != nil {
if webCfg, ok := svc.Web[hp]; ok {
return webCfg.Handlers[mount]
}
}
return nil
}
if sc.Web[hp] == nil {
return nil return nil
} }
return sc.Web[hp].Handlers[mount] return sc.Web[hp].Handlers[mount]
@ -182,10 +193,16 @@ func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler {
// GetTCPPortHandler returns the TCPPortHandler for the given port. // GetTCPPortHandler returns the TCPPortHandler for the given port.
// If the port is not configured, nil is returned. // If the port is not configured, nil is returned.
func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler { func (sc *ServeConfig) GetTCPPortHandler(port uint16, dnsName string) *TCPPortHandler {
if sc == nil { if sc == nil {
return nil return nil
} }
if IsServiceName(dnsName) {
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc != nil {
return svc.TCP[port]
}
return nil
}
return sc.TCP[port] return sc.TCP[port]
} }
@ -227,34 +244,78 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
return false return false
} }
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding // IsServiceName reports whether if the given string is a valid service name.
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving. func IsServiceName(s string) bool {
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool { return tailcfg.ServiceName(s).Validate() == nil
if sc == nil || sc.TCP[port] == nil { }
// IsTCPForwardingOnPort reports whether ServeConfig is currently forwarding
// in TCPForward mode on the given port for a DNSName. DNSName will be either node's DNSName, or a
// serviceName for service hosted on node. This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16, dnsName string) bool {
if sc == nil {
return false return false
} }
return !sc.IsServingWeb(port) forService := IsServiceName(dnsName)
if forService {
svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]
if !ok || svc == nil {
return false
}
if svc.TCP[port] == nil {
return false
}
} else if sc.TCP[port] == nil {
return false
}
return !sc.IsServingWeb(port, dnsName)
} }
// IsServingWeb reports whether if ServeConfig is currently serving Web // IsServingWeb reports whether ServeConfig is currently serving Web
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding. // (HTTP/HTTPS) on the given port for a DNSName. DNSName will be either node's DNSName, or a
func (sc *ServeConfig) IsServingWeb(port uint16) bool { // serviceName for service hosted on node. This is exclusive of TCPForwarding.
return sc.IsServingHTTP(port) || sc.IsServingHTTPS(port) func (sc *ServeConfig) IsServingWeb(port uint16, dnsName string) bool {
return sc.IsServingHTTP(port, dnsName) || sc.IsServingHTTPS(port, dnsName)
} }
// IsServingHTTPS reports whether if ServeConfig is currently serving HTTPS on // IsServingHTTPS reports whether ServeConfig is currently serving HTTPS on
// the given port. This is exclusive of HTTP and TCPForwarding. // the given port for a DNSName. DNSName will be either node's DNSName, or a
func (sc *ServeConfig) IsServingHTTPS(port uint16) bool { // serviceName for service hosted on node. This is exclusive of HTTP and TCPForwarding.
if sc == nil || sc.TCP[port] == nil { func (sc *ServeConfig) IsServingHTTPS(port uint16, dnsName string) bool {
if sc == nil {
return false
}
if IsServiceName(dnsName) {
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc != nil {
if svc.TCP[port] != nil {
return svc.TCP[port].HTTPS
}
}
return false
}
if sc.TCP[port] == nil {
return false return false
} }
return sc.TCP[port].HTTPS return sc.TCP[port].HTTPS
} }
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the // IsServingHTTP reports whether ServeConfig is currently serving HTTP on the
// given port. This is exclusive of HTTPS and TCPForwarding. // given port for a DNSName. DNSName will be either node's DNSName, or a
func (sc *ServeConfig) IsServingHTTP(port uint16) bool { // serviceName for service hosted on node. This is exclusive of HTTPS and TCPForwarding.
if sc == nil || sc.TCP[port] == nil { func (sc *ServeConfig) IsServingHTTP(port uint16, dnsName string) bool {
if sc == nil {
return false
}
if IsServiceName(dnsName) {
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc != nil {
if svc.TCP[port] != nil {
return svc.TCP[port].HTTP
}
}
return false
}
if sc.TCP[port] == nil {
return false return false
} }
return sc.TCP[port].HTTP return sc.TCP[port].HTTP
@ -278,23 +339,51 @@ func (sc *ServeConfig) FindConfig(port uint16) (*ServeConfig, bool) {
return nil, false return nil, false
} }
// FindServiceConfig finds the ServiceConfig for the given service name when it
// is hosting the given port.
func (sc *ServeConfig) FindServiceConfig(svcName tailcfg.ServiceName) *ServiceConfig {
if sc == nil {
return nil
}
if svc, ok := sc.Services[svcName]; ok && svc != nil {
return svc
}
return nil
}
// SetWebHandler sets the given HTTPHandler at the specified host, port, // SetWebHandler sets the given HTTPHandler at the specified host, port,
// and mount in the serve config. sc.TCP is also updated to reflect web // and mount in the serve config. sc.TCP is also updated to reflect web
// serving usage of the given port. // serving usage of the given port.
func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uint16, mount string, useTLS bool) { func (sc *ServeConfig) SetWebHandler(st *ipnstate.Status, handler *HTTPHandler, host string, port uint16, mount string, useTLS bool) {
if sc == nil { if sc == nil {
sc = new(ServeConfig) sc = new(ServeConfig)
} }
var hp HostPort
var webCfg *WebServerConfig
if IsServiceName(host) {
svcName := tailcfg.ServiceName(host)
dnsNameForService := svcName.WithoutPrefix() + "." + st.CurrentTailnet.MagicDNSSuffix
if _, ok := sc.Services[svcName]; !ok {
mak.Set(&sc.Services, svcName, new(ServiceConfig))
}
mak.Set(&sc.Services[svcName].TCP, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
hp = HostPort(net.JoinHostPort(dnsNameForService, strconv.Itoa(int(port))))
if _, ok := sc.Services[svcName].Web[hp]; !ok {
mak.Set(&sc.Services[svcName].Web, hp, new(WebServerConfig))
}
mak.Set(&sc.Services[svcName].Web[hp].Handlers, mount, handler)
webCfg = sc.Services[svcName].Web[hp]
} else {
mak.Set(&sc.TCP, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS}) mak.Set(&sc.TCP, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
hp = HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
if _, ok := sc.Web[hp]; !ok { if _, ok := sc.Web[hp]; !ok {
mak.Set(&sc.Web, hp, new(WebServerConfig)) mak.Set(&sc.Web, hp, new(WebServerConfig))
} }
mak.Set(&sc.Web[hp].Handlers, mount, handler) mak.Set(&sc.Web[hp].Handlers, mount, handler)
webCfg = sc.Web[hp]
}
// TODO(tylersmalley): handle multiple web handlers from foreground mode // TODO(tylersmalley): handle multiple web handlers from foreground mode
for k, v := range sc.Web[hp].Handlers { for k, v := range webCfg.Handlers {
if v == handler { if v == handler {
continue continue
} }
@ -305,9 +394,10 @@ func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uin
m1 := strings.TrimSuffix(mount, "/") m1 := strings.TrimSuffix(mount, "/")
m2 := strings.TrimSuffix(k, "/") m2 := strings.TrimSuffix(k, "/")
if m1 == m2 { if m1 == m2 {
delete(sc.Web[hp].Handlers, k) delete(webCfg.Handlers, k)
} }
} }
} }
// SetTCPForwarding sets the fwdAddr (IP:port form) to which to forward // SetTCPForwarding sets the fwdAddr (IP:port form) to which to forward
@ -318,9 +408,20 @@ func (sc *ServeConfig) SetTCPForwarding(port uint16, fwdAddr string, terminateTL
if sc == nil { if sc == nil {
sc = new(ServeConfig) sc = new(ServeConfig)
} }
var tcpPortHandler *TCPPortHandler
if IsServiceName(host) {
svcName := tailcfg.ServiceName(host)
if _, ok := sc.Services[svcName]; !ok {
mak.Set(&sc.Services, svcName, new(ServiceConfig))
}
mak.Set(&sc.Services[svcName].TCP, port, &TCPPortHandler{TCPForward: fwdAddr})
tcpPortHandler = sc.Services[svcName].TCP[port]
} else {
mak.Set(&sc.TCP, port, &TCPPortHandler{TCPForward: fwdAddr}) mak.Set(&sc.TCP, port, &TCPPortHandler{TCPForward: fwdAddr})
tcpPortHandler = sc.TCP[port]
}
if terminateTLS { if terminateTLS {
sc.TCP[port].TerminateTLS = host tcpPortHandler.TerminateTLS = host
} }
} }
@ -345,7 +446,7 @@ func (sc *ServeConfig) SetFunnel(host string, port uint16, setOn bool) {
} }
// RemoveWebHandler deletes the web handlers at all of the given mount points // RemoveWebHandler deletes the web handlers at all of the given mount points
// for the provided host and port in the serve config. If cleanupFunnel is // for the provided host and port in the serve config for node. If cleanupFunnel is
// true, this also removes the funnel value for this port if no handlers remain. // true, this also removes the funnel value for this port if no handlers remain.
func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []string, cleanupFunnel bool) { func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []string, cleanupFunnel bool) {
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port)))) hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
@ -374,9 +475,39 @@ func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []strin
} }
} }
// RemoveServiceWebHandler deletes the web handlers at all of the given mount points
// for the provided host and port in the serve config for the given service.
func (sc *ServeConfig) RemoveServiceWebHandler(st *ipnstate.Status, svcName tailcfg.ServiceName, port uint16, mounts []string) {
dnsNameForService := svcName.WithoutPrefix() + "." + st.CurrentTailnet.MagicDNSSuffix
hp := HostPort(net.JoinHostPort(dnsNameForService, strconv.Itoa(int(port))))
svc, ok := sc.Services[svcName]
if !ok || svc == nil {
return
}
// Delete existing handler, then cascade delete if empty.
for _, m := range mounts {
delete(svc.Web[hp].Handlers, m)
}
if len(svc.Web[hp].Handlers) == 0 {
delete(svc.Web, hp)
delete(svc.TCP, port)
}
}
// RemoveTCPForwarding deletes the TCP forwarding configuration for the given // RemoveTCPForwarding deletes the TCP forwarding configuration for the given
// port from the serve config. // port from the serve config.
func (sc *ServeConfig) RemoveTCPForwarding(port uint16) { func (sc *ServeConfig) RemoveTCPForwarding(dnsName string, port uint16) {
if IsServiceName(dnsName) {
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc != nil {
delete(svc.TCP, port)
if len(svc.TCP) == 0 {
svc.TCP = nil
}
}
return
}
delete(sc.TCP, port) delete(sc.TCP, port)
if len(sc.TCP) == 0 { if len(sc.TCP) == 0 {
sc.TCP = nil sc.TCP = nil

View File

@ -127,6 +127,121 @@ func TestHasPathHandler(t *testing.T) {
} }
} }
func TestIsTCPForwardingOnPort(t *testing.T) {
tests := []struct {
name string
cfg ServeConfig
dns string
port uint16
want bool
}{
{
name: "empty-config",
cfg: ServeConfig{},
dns: "foo.test.ts.net",
port: 80,
want: false,
},
{
name: "node-tcp-config-match",
cfg: ServeConfig{
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
},
dns: "foo.test.ts.net",
port: 80,
want: true,
},
{
name: "node-tcp-config-no-match",
cfg: ServeConfig{
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
},
dns: "foo.test.ts.net",
port: 443,
want: false,
},
{
name: "node-tcp-config-no-match-with-service",
cfg: ServeConfig{
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
},
dns: "svc:bar",
port: 80,
want: false,
},
{
name: "node-web-config-no-match",
cfg: ServeConfig{
TCP: map[uint16]*TCPPortHandler{80: {HTTPS: true}},
Web: map[HostPort]*WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*HTTPHandler{
"/": {Text: "Hello, world!"},
},
},
},
},
dns: "foo.test.ts.net",
port: 80,
want: false,
},
{
name: "service-tcp-config-match",
cfg: ServeConfig{
Services: map[tailcfg.ServiceName]*ServiceConfig{
"svc:foo": {
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
},
},
},
dns: "svc:foo",
port: 80,
want: true,
},
{
name: "service-tcp-config-no-match",
cfg: ServeConfig{
Services: map[tailcfg.ServiceName]*ServiceConfig{
"svc:foo": {
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
},
},
},
dns: "svc:bar",
port: 80,
want: false,
},
{
name: "service-web-config-no-match",
cfg: ServeConfig{
Services: map[tailcfg.ServiceName]*ServiceConfig{
"svc:foo": {
TCP: map[uint16]*TCPPortHandler{80: {HTTPS: true}},
Web: map[HostPort]*WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*HTTPHandler{
"/": {Text: "Hello, world!"},
},
},
},
},
},
},
dns: "svc:foo",
port: 80,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.cfg.IsTCPForwardingOnPort(tt.port, tt.dns)
if tt.want != got {
t.Errorf("IsTCPForwardingOnPort() = %v, want %v", got, tt.want)
}
})
}
}
func TestExpandProxyTargetDev(t *testing.T) { func TestExpandProxyTargetDev(t *testing.T) {
tests := []struct { tests := []struct {
name string name string