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

@ -47,6 +47,25 @@ func runAdvertise(ctx context.Context, args []string) error {
if err != nil {
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{
AdvertiseServicesSet: true,

@ -141,6 +141,7 @@ type localServeClient interface {
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, 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
@ -154,14 +155,16 @@ type serveEnv struct {
json bool // output JSON (status only for now)
// v2 specific flags
bg bool // background mode
setPath string // serve path
https uint // HTTP port
http uint // HTTP port
tcp uint // TCP port
tlsTerminatedTCP uint // a TLS terminated TCP port
subcmd serveMode // subcommand
yes bool // update without prompt
bg bgBoolFlag // background mode
setPath string // serve path
https uint // HTTP port
http uint // HTTP port
tcp uint // TCP port
tlsTerminatedTCP uint // a TLS terminated TCP port
subcmd serveMode // subcommand
yes bool // update without prompt
service string // service name
tun bool // redirect traffic to OS for service
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 {
return err
}
if sc.IsTCPForwardingOnPort(srvPort) {
if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
fmt.Fprintf(Stderr, "error: cannot serve web; already serving TCP\n")
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 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 {
return err
}
if sc.IsTCPForwardingOnPort(srvPort) {
if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
return errors.New("cannot remove web handler; currently serving TCP")
}
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")
}
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
if sc.IsServingWeb(srcPort) {
if sc.IsServingWeb(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 {
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)
}
if ph := sc.GetTCPPortHandler(src); ph != nil {
sc.RemoveTCPForwarding(src)
if ph := sc.GetTCPPortHandler(src, dnsName); ph != nil {
sc.RemoveTCPForwarding(dnsName, src)
return e.lc.SetServeConfig(ctx, sc)
}
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"
if sc.IsServingHTTP(port) {
if sc.IsServingHTTP(port, host) {
scheme = "http"
}

@ -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) {
return fakeStatus, nil
}
@ -891,6 +895,10 @@ func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.
return nil
}
func (lc *fakeLocalServeClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
return fakePref, nil
}
type mockQueryFeatureResponse struct {
resp *tailcfg.QueryFeatureResponse
err error

@ -18,6 +18,7 @@ import (
"os/signal"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
@ -40,6 +41,32 @@ type commandInfo struct {
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(`
<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),
@ -72,6 +99,7 @@ const (
serveTypeHTTP
serveTypeTCP
serveTypeTLSTerminatedTCP
serveTypeTun
)
var infoMap = map[serveMode]commandInfo{
@ -119,7 +147,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
Exec: e.runServeCombined(subcmd),
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.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
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.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.tun, "tun", false, "Forward all traffic to the local machine (default false), only supported for services")
}),
UsageFunc: usageFuncNoDefaultValues,
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")
return errHelpFunc(subcmd)
}
if len(args) == 0 && e.tun {
return nil
}
if len(args) == 0 {
return flag.ErrHelp
}
if e.tun && len(args) > 1 {
fmt.Fprintln(e.stderr(), "Error: invalid argument format")
return errHelpFunc(subcmd)
}
if len(args) > 2 {
fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args))
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.
func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
e.subcmd = subcmd
if !e.bg.SetByUser {
e.bg.Value = e.service != ""
}
return func(ctx context.Context, args []string) error {
// 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)
}
if err := e.validateArgs(subcmd, args); err != nil {
return err
}
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
funnel := subcmd == funnel
if e.service != "" && funnel {
return errors.New("Error: --service flag is not supported with funnel")
}
if funnel {
// verify node has funnel capabilities
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)
if err != nil {
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 {
fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
return errHelpFunc(subcmd)
@ -229,6 +275,11 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
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
if sc == nil {
sc = new(ipn.ServeConfig)
@ -245,7 +296,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
// foreground or background.
parentSC := sc
turnOff := "off" == args[len(args)-1]
turnOff := len(args) > 0 && "off" == args[len(args)-1]
if !turnOff && srvType == serveTypeHTTPS {
// Running serve with https requires that the tailnet has enabled
// https cert provisioning. Send users through an interactive flow
@ -262,10 +313,21 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
}
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 {
// 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
}
@ -291,13 +353,21 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
var msg string
if turnOff {
err = e.unsetServe(sc, dnsName, srvType, srvPort, mount)
if wasDefaultServe && forService {
delete(sc.Services, tailcfg.ServiceName(dnsName))
} else {
err = e.unsetServe(sc, st, dnsName, srvType, srvPort, mount)
}
} else {
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
if err := e.validateConfig(parentSC, srvPort, srvType, dnsName); err != nil {
return err
}
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
target := ""
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 {
fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
@ -333,20 +403,42 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
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 {
sc, isFg := sc.FindConfig(port)
if sc == nil {
return nil
// 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)
if sc == nil {
return nil
}
if isFg {
return errors.New("foreground already exists under this port")
}
if !e.bg.Value {
return fmt.Errorf(backgroundExistsMsg, infoMap[e.subcmd].Name, wantServe.String(), port)
}
tcpHandlerForPort = sc.TCP[port]
}
if isFg {
return errors.New("foreground already exists under this port")
}
if !e.bg {
return fmt.Errorf(backgroundExistsMsg, infoMap[e.subcmd].Name, wantServe.String(), port)
}
existingServe := serveFromPortHandler(sc.TCP[port])
existingServe := serveFromPortHandler(tcpHandlerForPort)
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
}
@ -371,7 +463,7 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
switch srvType {
case serveTypeHTTPS, serveTypeHTTP:
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 {
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 != "" {
return fmt.Errorf("cannot mount a path for TCP serve")
}
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
if err != nil {
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:
return fmt.Errorf("invalid type %q", srvType)
}
// update the serve config based on if funnel is enabled
e.applyFunnel(sc, dnsName, srvPort, allowFunnel)
// 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)
}
return nil
}
var (
msgFunnelAvailable = "Available on the internet:"
msgServeAvailable = "Available within your tailnet:"
msgRunningInBackground = "%s started and running in the background."
msgDisableProxy = "To disable the proxy, run: tailscale %s --%s=%d off"
msgToExit = "Press Ctrl+C to exit."
msgFunnelAvailable = "Available on the internet:"
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."
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"
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."
)
// messageForPort returns a message for the given port based on the
// 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
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
if sc.AllowFunnel[hp] == true {
output.WriteString(msgFunnelAvailable)
} else {
output.WriteString(msgServeAvailable)
forService := ipn.IsServiceName(dnsName)
var hp ipn.HostPort
var webConfig *ipn.WebServerConfig
var tcpHandler *ipn.TCPPortHandler
ips := st.TailscaleIPs
host := dnsName
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"
if sc.IsServingHTTP(srvPort) {
if sc.IsServingHTTP(srvPort, dnsName) {
scheme = "http"
}
@ -438,37 +544,70 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
}
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 {
mounts := slicesx.MapKeys(sc.Web[hp].Handlers)
if webConfig != nil {
mounts := slicesx.MapKeys(webConfig.Handlers)
sort.Slice(mounts, func(i, j int) bool {
return len(mounts[i]) < len(mounts[j])
})
for _, m := range mounts {
h := sc.Web[hp].Handlers[m]
h := webConfig.Handlers[m]
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))
}
} else if sc.TCP[srvPort] != nil {
h := sc.TCP[srvPort]
} else if tcpHandler != nil {
h := tcpHandler
tlsStatus := "TLS over TCP"
if h.TerminateTLS != "" {
tlsStatus = "TLS terminated"
}
output.WriteString(fmt.Sprintf("%s://%s%s\n", scheme, dnsName, portPart))
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)))
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)
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("\n")
output.WriteString(fmt.Sprintf(msgDisableProxy, subCmd, srvType.String(), srvPort))
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))
}
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)
switch {
case strings.HasPrefix(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
if sc.IsTCPForwardingOnPort(srvPort) {
if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
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
}
@ -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
if sc.IsServingWeb(srcPort) {
if sc.IsServingWeb(srcPort, dnsName) {
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.
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 {
case serveTypeHTTPS, serveTypeHTTP:
err := e.removeWebServe(sc, dnsName, srvPort, mount)
err := e.removeWebServe(sc, st, dnsName, srvPort, mount)
if err != nil {
return fmt.Errorf("failed to remove web serve: %w", err)
}
case serveTypeTCP, serveTypeTLSTerminatedTCP:
err := e.removeTCPServe(sc, srvPort)
err := e.removeTCPServe(sc, dnsName, srvPort)
if err != nil {
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:
return fmt.Errorf("invalid type %q", srvType)
}
@ -598,7 +752,7 @@ func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serve
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{
serveTypeHTTP: e.http,
serveTypeHTTPS: e.https,
@ -611,22 +765,30 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, er
for k, v := range sourceMap {
if v != 0 {
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++
srvType = k
srvPort = uint16(v)
wasDefault = false
}
}
if e.tun {
srcTypeCount++
srvType = serveTypeTun
wasDefault = false
}
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 {
srvType = serveTypeHTTPS
srvPort = 443
wasDefault = true
}
return srvType, srvPort, nil
return srvType, srvPort, wasDefault, nil
}
// 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.
// The srvPort argument is the serving port and the mount argument is
// the mount point or registered path to remove.
func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, mount string) error {
if sc.IsTCPForwardingOnPort(srvPort) {
return errors.New("cannot remove web handler; currently serving TCP")
func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16, mount string) error {
if sc == nil {
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))
hp := ipn.HostPort(net.JoinHostPort(dnsName, portStr))
if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
return errors.New("cannot remove web handler; currently serving TCP")
}
var targetExists bool
var mounts []string
// mount is deduced from e.setPath but it is ambiguous as
// to whether the user explicitly passed "/" or it was defaulted to.
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 {
for mount := range sc.Web[hp].Handlers {
for mount := range webServeMap[hp].Handlers {
mounts = append(mounts, mount)
}
}
} else {
targetExists = sc.WebHandlerExists(hp, mount)
targetExists = sc.WebHandlerExists(dnsName, hp, mount)
mounts = []string{mount}
}
@ -756,29 +936,49 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
}
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) {
return nil
}
}
sc.RemoveWebHandler(dnsName, srvPort, mounts, true)
if forService {
sc.RemoveServiceWebHandler(st, tailcfg.ServiceName(dnsName), srvPort, mounts)
} else {
sc.RemoveWebHandler(dnsName, srvPort, mounts, true)
}
return nil
}
// removeTCPServe removes the TCP forwarding configuration for the
// given srvPort, or serving port.
func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error {
// given srvPort, or serving port for the given dnsName.
func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, dnsName string, src uint16) error {
if sc == nil {
return nil
}
if sc.GetTCPPortHandler(src) == nil {
if sc.GetTCPPortHandler(src, dnsName) == nil {
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)
}
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
}

@ -8,6 +8,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/netip"
"os"
"path/filepath"
"reflect"
@ -19,6 +20,7 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
func TestServeDevConfigMutations(t *testing.T) {
@ -874,15 +876,17 @@ func TestValidateConfig(t *testing.T) {
name string
desc string
cfg *ipn.ServeConfig
dns string
servePort uint16
serveType serveType
bg bool
bg bgBoolFlag
wantErr bool
}{
{
name: "nil_config",
desc: "when config is nil, all requests valid",
cfg: nil,
dns: "node.test.ts.net",
servePort: 3000,
serveType: serveTypeHTTPS,
},
@ -894,7 +898,8 @@ func TestValidateConfig(t *testing.T) {
443: {HTTPS: true},
},
},
bg: true,
dns: "node.test.ts.net",
bg: bgBoolFlag{true, false},
servePort: 10000,
serveType: serveTypeHTTPS,
},
@ -906,7 +911,8 @@ func TestValidateConfig(t *testing.T) {
443: {TCPForward: "http://localhost:4545"},
},
},
bg: true,
dns: "node.test.ts.net",
bg: bgBoolFlag{true, false},
servePort: 443,
serveType: serveTypeTCP,
},
@ -918,7 +924,8 @@ func TestValidateConfig(t *testing.T) {
443: {HTTPS: true},
},
},
bg: true,
dns: "node.test.ts.net",
bg: bgBoolFlag{true, false},
servePort: 443,
serveType: serveTypeHTTP,
wantErr: true,
@ -938,6 +945,7 @@ func TestValidateConfig(t *testing.T) {
},
},
},
dns: "node.test.ts.net",
servePort: 4040,
serveType: serveTypeTCP,
},
@ -953,16 +961,95 @@ func TestValidateConfig(t *testing.T) {
},
},
},
dns: "node.test.ts.net",
servePort: 3000,
serveType: serveTypeTCP,
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 {
t.Run(tc.name, func(t *testing.T) {
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 {
t.Fatal("expected an error but got nil")
}
@ -976,58 +1063,65 @@ func TestValidateConfig(t *testing.T) {
func TestSrcTypeFromFlags(t *testing.T) {
tests := []struct {
name string
env *serveEnv
expectedType serveType
expectedPort uint16
expectedErr bool
name string
env *serveEnv
expectedType serveType
expectedPort uint16
expectedErr bool
expectedWasDefault bool
}{
{
name: "only http set",
env: &serveEnv{http: 80},
expectedType: serveTypeHTTP,
expectedPort: 80,
expectedErr: false,
name: "only http set",
env: &serveEnv{http: 80},
expectedType: serveTypeHTTP,
expectedPort: 80,
expectedErr: false,
expectedWasDefault: false,
},
{
name: "only https set",
env: &serveEnv{https: 10000},
expectedType: serveTypeHTTPS,
expectedPort: 10000,
expectedErr: false,
name: "only https set",
env: &serveEnv{https: 10000},
expectedType: serveTypeHTTPS,
expectedPort: 10000,
expectedErr: false,
expectedWasDefault: false,
},
{
name: "only tcp set",
env: &serveEnv{tcp: 8000},
expectedType: serveTypeTCP,
expectedPort: 8000,
expectedErr: false,
name: "only tcp set",
env: &serveEnv{tcp: 8000},
expectedType: serveTypeTCP,
expectedPort: 8000,
expectedErr: false,
expectedWasDefault: false,
},
{
name: "only tls-terminated-tcp set",
env: &serveEnv{tlsTerminatedTCP: 8080},
expectedType: serveTypeTLSTerminatedTCP,
expectedPort: 8080,
expectedErr: false,
name: "only tls-terminated-tcp set",
env: &serveEnv{tlsTerminatedTCP: 8080},
expectedType: serveTypeTLSTerminatedTCP,
expectedPort: 8080,
expectedErr: false,
expectedWasDefault: false,
},
{
name: "defaults to https, port 443",
env: &serveEnv{},
expectedType: serveTypeHTTPS,
expectedPort: 443,
expectedErr: false,
name: "defaults to https, port 443",
env: &serveEnv{},
expectedType: serveTypeHTTPS,
expectedPort: 443,
expectedErr: false,
expectedWasDefault: true,
},
{
name: "multiple types set",
env: &serveEnv{http: 80, https: 443},
expectedPort: 0,
expectedErr: true,
name: "multiple types set",
env: &serveEnv{http: 80, https: 443},
expectedPort: 0,
expectedErr: true,
expectedWasDefault: false,
},
}
for _, tt := range tests {
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 {
t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err)
}
@ -1037,6 +1131,9 @@ func TestSrcTypeFromFlags(t *testing.T) {
if srcPort != tt.expectedPort {
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) {
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 {
name string
subcmd serveMode
serveConfig *ipn.ServeConfig
status *ipnstate.Status
prefs *ipn.Prefs
dnsName string
srvType serveType
srvPort uint16
@ -1147,13 +1254,252 @@ func TestMessageForPort(t *testing.T) {
fmt.Sprintf(msgDisableProxy, "serve", "http", 80),
}, "\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 {
e := &serveEnv{bg: true, subcmd: tt.subcmd}
e := &serveEnv{bg: bgBoolFlag{true, false}, subcmd: tt.subcmd}
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 == "" {
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.
// If optName is non-empty, it's used in the error message.
func exactErrMsg(want error) func(error) string {

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

@ -264,7 +264,7 @@ func serveOnLocalTailscaled(ctx context.Context, lc *local.Client, st *ipnstate.
fmt.Printf("setting funnel for %s:%v\n", serverURL, dstPort)
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)))),
}, serverURL, uint16(*flagPort), "/", true)
err = lc.SetServeConfig(ctx, sc)

@ -166,15 +166,26 @@ type HTTPHandler struct {
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
// the given host:port and mount point.
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
h := sc.GetWebHandler(hp, mount)
func (sc *ServeConfig) WebHandlerExists(dnsName string, hp HostPort, mount string) bool {
h := sc.GetWebHandler(dnsName, hp, mount)
return h != nil
}
// GetWebHandler returns the HTTPHandler for the given host:port and mount point.
// Returns nil if the handler does not exist.
func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler {
if sc == nil || sc.Web[hp] == nil {
func (sc *ServeConfig) GetWebHandler(dnsName string, hp HostPort, mount string) *HTTPHandler {
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 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.
// 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 {
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]
}
@ -227,34 +244,78 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
return false
}
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
// IsServiceName reports whether if the given string is a valid service name.
func IsServiceName(s string) bool {
return tailcfg.ServiceName(s).Validate() == 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 !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
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding.
func (sc *ServeConfig) IsServingWeb(port uint16) bool {
return sc.IsServingHTTP(port) || sc.IsServingHTTPS(port)
// IsServingWeb reports whether ServeConfig is currently serving Web
// (HTTP/HTTPS) 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 TCPForwarding.
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
// the given port. This is exclusive of HTTP and TCPForwarding.
func (sc *ServeConfig) IsServingHTTPS(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
// IsServingHTTPS reports whether ServeConfig is currently serving HTTPS 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 HTTP and TCPForwarding.
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 sc.TCP[port].HTTPS
}
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the
// given port. This is exclusive of HTTPS and TCPForwarding.
func (sc *ServeConfig) IsServingHTTP(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
// IsServingHTTP reports whether ServeConfig is currently serving HTTP 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 HTTPS and TCPForwarding.
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 sc.TCP[port].HTTP
@ -278,23 +339,51 @@ func (sc *ServeConfig) FindConfig(port uint16) (*ServeConfig, bool) {
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,
// and mount in the serve config. sc.TCP is also updated to reflect web
// 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 {
sc = new(ServeConfig)
}
mak.Set(&sc.TCP, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
if _, ok := sc.Web[hp]; !ok {
mak.Set(&sc.Web, hp, new(WebServerConfig))
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})
hp = HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
if _, ok := sc.Web[hp]; !ok {
mak.Set(&sc.Web, hp, new(WebServerConfig))
}
mak.Set(&sc.Web[hp].Handlers, mount, handler)
webCfg = sc.Web[hp]
}
mak.Set(&sc.Web[hp].Handlers, mount, handler)
// 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 {
continue
}
@ -305,9 +394,10 @@ func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uin
m1 := strings.TrimSuffix(mount, "/")
m2 := strings.TrimSuffix(k, "/")
if m1 == m2 {
delete(sc.Web[hp].Handlers, k)
delete(webCfg.Handlers, k)
}
}
}
// 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 {
sc = new(ServeConfig)
}
mak.Set(&sc.TCP, port, &TCPPortHandler{TCPForward: fwdAddr})
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})
tcpPortHandler = sc.TCP[port]
}
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
// 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.
func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []string, cleanupFunnel bool) {
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
// 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)
if len(sc.TCP) == 0 {
sc.TCP = nil

@ -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) {
tests := []struct {
name string