mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-25 18:51:01 +00:00
cmd/tailscale/cli: Allow tailscale serve serving services from cli
This commit adds flag to tailscale serve to enable serving a service on current node as a service host. Also added some message output to both serve and advertise for "next step" guidance. Updates tailscale/corp#22954 Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
parent
f2f7fd12eb
commit
2244badf0d
@ -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://"
|
||||
|
@ -263,7 +263,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)
|
||||
|
203
ipn/serve.go
203
ipn/serve.go
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user