mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-31 00:03:47 +00:00
cmd/tailscale/cli: Add service flag to serve command
This commit adds the service flag to serve command which allows serving a service and add the service to the advertisedServices field in prefs (What advertise command does that will be removed later). When adding proxies, TCP proxies and WEB proxies work the same way as normal serve, just under a different DNSname. There is a services specific L3 serving mode called Tun, can be set via --tun flag. Serving a service is always in --bg mode. If --bg is explicitly set t o false, an error message will be sent out. The restriction on proxy target being localhost or 127.0.0.1 also applies to services. When removing proxies, TCP proxies can be removed with type and port flag and off argument. Web proxies can be removed with type, port, setPath flag and off argument. To align with normal serve, when setPath is not set, all handler under the hostport will be removed. When flags are not set but off argument was passed by user, it will be a noop. Removing all config for a service will be available later with a new subcommand clear. Updates tailscale/corp#22954 Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
parent
d2c1ed22c3
commit
b8f2e27675
@ -141,6 +141,8 @@ type localServeClient interface {
|
|||||||
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
|
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
|
||||||
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error)
|
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error)
|
||||||
IncrementCounter(ctx context.Context, name string, delta int) error
|
IncrementCounter(ctx context.Context, name string, delta int) error
|
||||||
|
GetPrefs(ctx context.Context) (*ipn.Prefs, error)
|
||||||
|
EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveEnv is the environment the serve command runs within. All I/O should be
|
// serveEnv is the environment the serve command runs within. All I/O should be
|
||||||
@ -154,14 +156,16 @@ type serveEnv struct {
|
|||||||
json bool // output JSON (status only for now)
|
json bool // output JSON (status only for now)
|
||||||
|
|
||||||
// v2 specific flags
|
// v2 specific flags
|
||||||
bg bool // background mode
|
bg bgBoolFlag // background mode
|
||||||
setPath string // serve path
|
setPath string // serve path
|
||||||
https uint // HTTP port
|
https uint // HTTP port
|
||||||
http uint // HTTP port
|
http uint // HTTP port
|
||||||
tcp uint // TCP port
|
tcp uint // TCP port
|
||||||
tlsTerminatedTCP uint // a TLS terminated TCP port
|
tlsTerminatedTCP uint // a TLS terminated TCP port
|
||||||
subcmd serveMode // subcommand
|
subcmd serveMode // subcommand
|
||||||
yes bool // update without prompt
|
yes bool // update without prompt
|
||||||
|
service string // service name
|
||||||
|
tun bool // redirect traffic to OS for service
|
||||||
|
|
||||||
lc localServeClient // localClient interface, specific to serve
|
lc localServeClient // localClient interface, specific to serve
|
||||||
|
|
||||||
@ -354,12 +358,16 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
|
||||||
fmt.Fprintf(Stderr, "error: cannot serve web; already serving TCP\n")
|
fmt.Fprintf(Stderr, "error: cannot serve web; already serving TCP\n")
|
||||||
return errHelp
|
return errHelp
|
||||||
}
|
}
|
||||||
|
|
||||||
sc.SetWebHandler(h, dnsName, srvPort, mount, useTLS)
|
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting client status: %w", err)
|
||||||
|
}
|
||||||
|
sc.SetWebHandler(st, h, dnsName, srvPort, mount, useTLS)
|
||||||
|
|
||||||
if !reflect.DeepEqual(cursc, sc) {
|
if !reflect.DeepEqual(cursc, sc) {
|
||||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||||
@ -411,11 +419,11 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mou
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
|
||||||
return errors.New("cannot remove web handler; currently serving TCP")
|
return errors.New("cannot remove web handler; currently serving TCP")
|
||||||
}
|
}
|
||||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||||
if !sc.WebHandlerExists(hp, mount) {
|
if !sc.WebHandlerExists(dnsName, hp, mount) {
|
||||||
return errors.New("error: handler does not exist")
|
return errors.New("error: handler does not exist")
|
||||||
}
|
}
|
||||||
sc.RemoveWebHandler(dnsName, srvPort, []string{mount}, false)
|
sc.RemoveWebHandler(dnsName, srvPort, []string{mount}, false)
|
||||||
@ -550,7 +558,7 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
|
|||||||
|
|
||||||
fwdAddr := "127.0.0.1:" + dstPortStr
|
fwdAddr := "127.0.0.1:" + dstPortStr
|
||||||
|
|
||||||
if sc.IsServingWeb(srcPort) {
|
if sc.IsServingWeb(srcPort, "") {
|
||||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -581,11 +589,15 @@ func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
|
|||||||
if sc == nil {
|
if sc == nil {
|
||||||
sc = new(ipn.ServeConfig)
|
sc = new(ipn.ServeConfig)
|
||||||
}
|
}
|
||||||
if sc.IsServingWeb(src) {
|
dnsName, err := e.getSelfDNSName(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sc.IsServingWeb(src, dnsName) {
|
||||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
||||||
}
|
}
|
||||||
if ph := sc.GetTCPPortHandler(src); ph != nil {
|
if ph := sc.GetTCPPortHandler(src, dnsName); ph != nil {
|
||||||
sc.RemoveTCPForwarding(src)
|
sc.RemoveTCPForwarding(dnsName, src)
|
||||||
return e.lc.SetServeConfig(ctx, sc)
|
return e.lc.SetServeConfig(ctx, sc)
|
||||||
}
|
}
|
||||||
return errors.New("error: serve config does not exist")
|
return errors.New("error: serve config does not exist")
|
||||||
@ -682,7 +694,7 @@ func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
scheme := "https"
|
scheme := "https"
|
||||||
if sc.IsServingHTTP(port) {
|
if sc.IsServingHTTP(port, host) {
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -859,6 +859,7 @@ type fakeLocalServeClient struct {
|
|||||||
config *ipn.ServeConfig
|
config *ipn.ServeConfig
|
||||||
setCount int // counts calls to SetServeConfig
|
setCount int // counts calls to SetServeConfig
|
||||||
queryFeatureResponse *mockQueryFeatureResponse // mock response to QueryFeature calls
|
queryFeatureResponse *mockQueryFeatureResponse // mock response to QueryFeature calls
|
||||||
|
prefs *ipn.Prefs // fake preferences, used to test GetPrefs and SetPrefs
|
||||||
}
|
}
|
||||||
|
|
||||||
// fakeStatus is a fake ipnstate.Status value for tests.
|
// fakeStatus is a fake ipnstate.Status value for tests.
|
||||||
@ -891,6 +892,23 @@ func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (lc *fakeLocalServeClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
||||||
|
if lc.prefs == nil {
|
||||||
|
lc.prefs = ipn.NewPrefs()
|
||||||
|
}
|
||||||
|
return lc.prefs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *fakeLocalServeClient) EditPrefs(ctx context.Context, prefs *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||||
|
if lc.prefs == nil {
|
||||||
|
lc.prefs = ipn.NewPrefs()
|
||||||
|
}
|
||||||
|
if prefs.AdvertiseServicesSet {
|
||||||
|
lc.prefs.AdvertiseServices = prefs.AdvertiseServices
|
||||||
|
}
|
||||||
|
return lc.prefs, nil
|
||||||
|
}
|
||||||
|
|
||||||
type mockQueryFeatureResponse struct {
|
type mockQueryFeatureResponse struct {
|
||||||
resp *tailcfg.QueryFeatureResponse
|
resp *tailcfg.QueryFeatureResponse
|
||||||
err error
|
err error
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -41,6 +42,32 @@ type commandInfo struct {
|
|||||||
LongHelp string
|
LongHelp string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bgBoolFlag struct {
|
||||||
|
Value bool
|
||||||
|
SetByUser bool // tracks if the flag was set by the user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the boolean flag and wether it's explicitly set by user based on the string value.
|
||||||
|
func (b *bgBoolFlag) Set(s string) error {
|
||||||
|
if s == "true" {
|
||||||
|
b.Value = true
|
||||||
|
} else if s == "false" {
|
||||||
|
b.Value = false
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("invalid boolean value: %s", s)
|
||||||
|
}
|
||||||
|
b.SetByUser = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a hack to make the flag package recognize that this is a boolean flag.
|
||||||
|
func (b *bgBoolFlag) IsBoolFlag() bool { return true }
|
||||||
|
|
||||||
|
// String returns the string representation of the boolean flag.
|
||||||
|
func (b *bgBoolFlag) String() string {
|
||||||
|
return fmt.Sprintf("%t", b.Value)
|
||||||
|
}
|
||||||
|
|
||||||
var serveHelpCommon = strings.TrimSpace(`
|
var serveHelpCommon = strings.TrimSpace(`
|
||||||
<target> can be a file, directory, text, or most commonly the location to a service running on the
|
<target> can be a file, directory, text, or most commonly the location to a service running on the
|
||||||
local machine. The location to the location service can be expressed as a port number (e.g., 3000),
|
local machine. The location to the location service can be expressed as a port number (e.g., 3000),
|
||||||
@ -73,6 +100,7 @@ const (
|
|||||||
serveTypeHTTP
|
serveTypeHTTP
|
||||||
serveTypeTCP
|
serveTypeTCP
|
||||||
serveTypeTLSTerminatedTCP
|
serveTypeTLSTerminatedTCP
|
||||||
|
serveTypeTun
|
||||||
)
|
)
|
||||||
|
|
||||||
var infoMap = map[serveMode]commandInfo{
|
var infoMap = map[serveMode]commandInfo{
|
||||||
@ -120,7 +148,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
|||||||
Exec: e.runServeCombined(subcmd),
|
Exec: e.runServeCombined(subcmd),
|
||||||
|
|
||||||
FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) {
|
FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) {
|
||||||
fs.BoolVar(&e.bg, "bg", false, "Run the command as a background process (default false)")
|
fs.Var(&e.bg, "bg", "Run the command as a background process (default false)")
|
||||||
fs.StringVar(&e.setPath, "set-path", "", "Appends the specified path to the base URL for accessing the underlying service")
|
fs.StringVar(&e.setPath, "set-path", "", "Appends the specified path to the base URL for accessing the underlying service")
|
||||||
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
|
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
|
||||||
if subcmd == serve {
|
if subcmd == serve {
|
||||||
@ -128,7 +156,9 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
|||||||
}
|
}
|
||||||
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
||||||
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
||||||
|
fs.StringVar(&e.service, "service", "", "Name of the service to serve.")
|
||||||
fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts (default false)")
|
fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts (default false)")
|
||||||
|
fs.BoolVar(&e.tun, "tun", false, "Forward all traffic to the local machine (default false), only supported for services")
|
||||||
}),
|
}),
|
||||||
UsageFunc: usageFuncNoDefaultValues,
|
UsageFunc: usageFuncNoDefaultValues,
|
||||||
Subcommands: []*ffcli.Command{
|
Subcommands: []*ffcli.Command{
|
||||||
@ -162,9 +192,16 @@ func (e *serveEnv) validateArgs(subcmd serveMode, args []string) error {
|
|||||||
fmt.Fprint(e.stderr(), "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n")
|
fmt.Fprint(e.stderr(), "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n")
|
||||||
return errHelpFunc(subcmd)
|
return errHelpFunc(subcmd)
|
||||||
}
|
}
|
||||||
|
if len(args) == 0 && e.tun {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return flag.ErrHelp
|
return flag.ErrHelp
|
||||||
}
|
}
|
||||||
|
if e.tun && len(args) > 1 {
|
||||||
|
fmt.Fprintln(e.stderr(), "Error: invalid argument format")
|
||||||
|
return errHelpFunc(subcmd)
|
||||||
|
}
|
||||||
if len(args) > 2 {
|
if len(args) > 2 {
|
||||||
fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args))
|
fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args))
|
||||||
return errHelpFunc(subcmd)
|
return errHelpFunc(subcmd)
|
||||||
@ -183,6 +220,9 @@ func (e *serveEnv) validateArgs(subcmd serveMode, args []string) error {
|
|||||||
// runServeCombined is the entry point for the "tailscale {serve,funnel}" commands.
|
// runServeCombined is the entry point for the "tailscale {serve,funnel}" commands.
|
||||||
func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||||
e.subcmd = subcmd
|
e.subcmd = subcmd
|
||||||
|
if !e.bg.SetByUser {
|
||||||
|
e.bg.Value = e.service != ""
|
||||||
|
}
|
||||||
|
|
||||||
return func(ctx context.Context, args []string) error {
|
return func(ctx context.Context, args []string) error {
|
||||||
// Undocumented debug command (not using ffcli subcommands) to set raw
|
// Undocumented debug command (not using ffcli subcommands) to set raw
|
||||||
@ -198,15 +238,17 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
|||||||
}
|
}
|
||||||
return e.lc.SetServeConfig(ctx, sc)
|
return e.lc.SetServeConfig(ctx, sc)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := e.validateArgs(subcmd, args); err != nil {
|
if err := e.validateArgs(subcmd, args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
funnel := subcmd == funnel
|
funnel := subcmd == funnel
|
||||||
|
if e.service != "" && funnel {
|
||||||
|
return errors.New("Error: --service flag is not supported with funnel")
|
||||||
|
}
|
||||||
|
|
||||||
if funnel {
|
if funnel {
|
||||||
// verify node has funnel capabilities
|
// verify node has funnel capabilities
|
||||||
if err := e.verifyFunnelEnabled(ctx, 443); err != nil {
|
if err := e.verifyFunnelEnabled(ctx, 443); err != nil {
|
||||||
@ -214,12 +256,16 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.service != "" && e.bg.SetByUser && !e.bg.Value {
|
||||||
|
return errors.New("Error: --service flag is only compatible with background mode")
|
||||||
|
}
|
||||||
|
|
||||||
mount, err := cleanURLPath(e.setPath)
|
mount, err := cleanURLPath(e.setPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to clean the mount point: %w", err)
|
return fmt.Errorf("failed to clean the mount point: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srvType, srvPort, err := srvTypeAndPortFromFlags(e)
|
srvType, srvPort, wasDefaultServe, err := srvTypeAndPortFromFlags(e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
|
fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
|
||||||
return errHelpFunc(subcmd)
|
return errHelpFunc(subcmd)
|
||||||
@ -246,7 +292,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
|||||||
// foreground or background.
|
// foreground or background.
|
||||||
parentSC := sc
|
parentSC := sc
|
||||||
|
|
||||||
turnOff := "off" == args[len(args)-1]
|
turnOff := len(args) > 0 && "off" == args[len(args)-1]
|
||||||
if !turnOff && srvType == serveTypeHTTPS {
|
if !turnOff && srvType == serveTypeHTTPS {
|
||||||
// Running serve with https requires that the tailnet has enabled
|
// Running serve with https requires that the tailnet has enabled
|
||||||
// https cert provisioning. Send users through an interactive flow
|
// https cert provisioning. Send users through an interactive flow
|
||||||
@ -263,10 +309,21 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var watcher *tailscale.IPNBusWatcher
|
var watcher *tailscale.IPNBusWatcher
|
||||||
wantFg := !e.bg && !turnOff
|
forService := e.service != ""
|
||||||
|
if forService {
|
||||||
|
err = tailcfg.ServiceName(e.service).Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse service name: %w", err)
|
||||||
|
}
|
||||||
|
dnsName = e.service
|
||||||
|
}
|
||||||
|
if !forService && srvType == serveTypeTun {
|
||||||
|
return errors.New("tun mode is only supported for services")
|
||||||
|
}
|
||||||
|
wantFg := !forService && !e.bg.Value && !turnOff
|
||||||
if wantFg {
|
if wantFg {
|
||||||
// validate the config before creating a WatchIPNBus session
|
// validate the config before creating a WatchIPNBus session
|
||||||
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
if err := e.validateConfig(parentSC, srvPort, srvType, dnsName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,13 +348,19 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var msg string
|
var msg string
|
||||||
if turnOff {
|
if turnOff && !wasDefaultServe {
|
||||||
err = e.unsetServe(sc, dnsName, srvType, srvPort, mount)
|
// only unset serve when trying to unset with type and port flags.
|
||||||
|
err = e.unsetServe(sc, st, dnsName, srvType, srvPort, mount)
|
||||||
} else {
|
} else {
|
||||||
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
if err := e.validateConfig(parentSC, srvPort, srvType, dnsName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
|
e.addServiceToPrefs(ctx, dnsName)
|
||||||
|
target := ""
|
||||||
|
if len(args) > 0 {
|
||||||
|
target = args[0]
|
||||||
|
}
|
||||||
|
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, target, funnel)
|
||||||
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -332,22 +395,63 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *serveEnv) addServiceToPrefs(ctx context.Context, serviceName string) error {
|
||||||
|
prefs, err := e.lc.GetPrefs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting prefs: %w", err)
|
||||||
|
}
|
||||||
|
advertisedServices := prefs.AdvertiseServices
|
||||||
|
if slices.Contains(advertisedServices, serviceName) {
|
||||||
|
return nil // already advertised
|
||||||
|
}
|
||||||
|
advertisedServices = append(advertisedServices, serviceName)
|
||||||
|
_, err = e.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||||
|
AdvertiseServicesSet: true,
|
||||||
|
Prefs: ipn.Prefs{
|
||||||
|
AdvertiseServices: advertisedServices,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const backgroundExistsMsg = "background configuration already exists, use `tailscale %s --%s=%d off` to remove the existing configuration"
|
const backgroundExistsMsg = "background configuration already exists, use `tailscale %s --%s=%d off` to remove the existing configuration"
|
||||||
|
|
||||||
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType) error {
|
// validateConfig checks if the serve config is valid to serve the type wanted on the port.
|
||||||
sc, isFg := sc.FindConfig(port)
|
// dnsName is a FQDN or a serviceName (with `svc:` prefix).
|
||||||
if sc == nil {
|
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType, dnsName string) error {
|
||||||
return nil
|
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 {
|
existingServe := serveFromPortHandler(tcpHandlerForPort)
|
||||||
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])
|
|
||||||
if wantServe != existingServe {
|
if wantServe != existingServe {
|
||||||
return fmt.Errorf("want %q but port is already serving %q", wantServe, existingServe)
|
return fmt.Errorf("want to serve %q but port is already serving %q for %q", wantServe, existingServe, dnsName)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -372,7 +476,7 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
|
|||||||
switch srvType {
|
switch srvType {
|
||||||
case serveTypeHTTPS, serveTypeHTTP:
|
case serveTypeHTTPS, serveTypeHTTP:
|
||||||
useTLS := srvType == serveTypeHTTPS
|
useTLS := srvType == serveTypeHTTPS
|
||||||
err := e.applyWebServe(sc, dnsName, srvPort, useTLS, mount, target)
|
err := e.applyWebServe(sc, st, dnsName, srvPort, useTLS, mount, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed apply web serve: %w", err)
|
return fmt.Errorf("failed apply web serve: %w", err)
|
||||||
}
|
}
|
||||||
@ -380,45 +484,58 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
|
|||||||
if e.setPath != "" {
|
if e.setPath != "" {
|
||||||
return fmt.Errorf("cannot mount a path for TCP serve")
|
return fmt.Errorf("cannot mount a path for TCP serve")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
|
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to apply TCP serve: %w", err)
|
return fmt.Errorf("failed to apply TCP serve: %w", err)
|
||||||
}
|
}
|
||||||
|
case serveTypeTun:
|
||||||
|
svcName := tailcfg.ServiceName(dnsName)
|
||||||
|
if _, ok := sc.Services[svcName]; !ok {
|
||||||
|
mak.Set(&sc.Services, svcName, new(ipn.ServiceConfig))
|
||||||
|
}
|
||||||
|
sc.Services[svcName].Tun = true
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid type %q", srvType)
|
return fmt.Errorf("invalid type %q", srvType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the serve config based on if funnel is enabled
|
// update the serve config based on if funnel is enabled
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
msgFunnelAvailable = "Available on the internet:"
|
msgFunnelAvailable = "Available on the internet:"
|
||||||
msgServeAvailable = "Available within your tailnet:"
|
msgServeAvailable = "Available within your tailnet:"
|
||||||
msgRunningInBackground = "%s started and running in the background."
|
msgServiceIPNotAssigned = "This service doesn't have VIPs assigned yet, once VIP is assigned, it will be available in your Tailnet as:"
|
||||||
msgDisableProxy = "To disable the proxy, run: tailscale %s --%s=%d off"
|
msgRunningInBackground = "%s started and running in the background."
|
||||||
msgToExit = "Press Ctrl+C to exit."
|
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 remove config for the service, run: tailscale serve clear --service=%s"
|
||||||
|
msgToExit = "Press Ctrl+C to exit."
|
||||||
)
|
)
|
||||||
|
|
||||||
// messageForPort returns a message for the given port based on the
|
// messageForPort returns a message for the given port based on the
|
||||||
// serve config and status.
|
// serve config and status.
|
||||||
func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16) string {
|
func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16) string {
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
|
forService := ipn.IsServiceName(dnsName)
|
||||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
var hp ipn.HostPort
|
||||||
|
var webConfig *ipn.WebServerConfig
|
||||||
if sc.AllowFunnel[hp] == true {
|
var tcpHandler *ipn.TCPPortHandler
|
||||||
output.WriteString(msgFunnelAvailable)
|
ips := st.TailscaleIPs
|
||||||
} else {
|
host := dnsName
|
||||||
output.WriteString(msgServeAvailable)
|
if forService {
|
||||||
|
host = tailcfg.ServiceName(dnsName).WithoutPrefix() + "." + st.CurrentTailnet.MagicDNSSuffix
|
||||||
}
|
}
|
||||||
output.WriteString("\n\n")
|
hp = ipn.HostPort(net.JoinHostPort(host, strconv.Itoa(int(srvPort))))
|
||||||
|
|
||||||
scheme := "https"
|
scheme := "https"
|
||||||
if sc.IsServingHTTP(srvPort) {
|
if sc.IsServingHTTP(srvPort, dnsName) {
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,37 +556,72 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
|||||||
}
|
}
|
||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
if forService {
|
||||||
|
svcName := tailcfg.ServiceName(dnsName)
|
||||||
|
serviceIPMaps, err := tailcfg.UnmarshalNodeCapJSON[tailcfg.ServiceIPMappings](st.Self.CapMap, tailcfg.NodeAttrServiceHost)
|
||||||
|
if err != nil || len(serviceIPMaps) == 0 || serviceIPMaps[0][svcName] == nil {
|
||||||
|
// The capmap does not contain IPs for this service yet. Usually this means
|
||||||
|
// the service hasn't been added to prefs and sent to control yet.
|
||||||
|
output.WriteString(msgServiceIPNotAssigned)
|
||||||
|
ips = nil
|
||||||
|
} else {
|
||||||
|
output.WriteString(msgServeAvailable)
|
||||||
|
ips = serviceIPMaps[0][svcName]
|
||||||
|
}
|
||||||
|
output.WriteString("\n\n")
|
||||||
|
svc := sc.FindServiceConfig(svcName)
|
||||||
|
if srvType == serveTypeTun && svc.Tun {
|
||||||
|
output.WriteString(fmt.Sprintf(msgRunningTunServie, host))
|
||||||
|
output.WriteString("\n")
|
||||||
|
output.WriteString(fmt.Sprintf(msgDisableServiceTun, dnsName))
|
||||||
|
output.WriteString("\n")
|
||||||
|
output.WriteString(fmt.Sprintf(msgDisableService, dnsName))
|
||||||
|
return output.String()
|
||||||
|
}
|
||||||
|
if svc != nil {
|
||||||
|
webConfig = svc.Web[hp]
|
||||||
|
tcpHandler = svc.TCP[srvPort]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if sc.AllowFunnel[hp] == true {
|
||||||
|
output.WriteString(msgFunnelAvailable)
|
||||||
|
} else {
|
||||||
|
output.WriteString(msgServeAvailable)
|
||||||
|
}
|
||||||
|
output.WriteString("\n\n")
|
||||||
|
webConfig = sc.Web[hp]
|
||||||
|
tcpHandler = sc.TCP[srvPort]
|
||||||
|
}
|
||||||
|
|
||||||
if sc.Web[hp] != nil {
|
if webConfig != nil {
|
||||||
mounts := slicesx.MapKeys(sc.Web[hp].Handlers)
|
mounts := slicesx.MapKeys(webConfig.Handlers)
|
||||||
sort.Slice(mounts, func(i, j int) bool {
|
sort.Slice(mounts, func(i, j int) bool {
|
||||||
return len(mounts[i]) < len(mounts[j])
|
return len(mounts[i]) < len(mounts[j])
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, m := range mounts {
|
for _, m := range mounts {
|
||||||
h := sc.Web[hp].Handlers[m]
|
h := webConfig.Handlers[m]
|
||||||
t, d := srvTypeAndDesc(h)
|
t, d := srvTypeAndDesc(h)
|
||||||
output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, dnsName, portPart, m))
|
output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, host, portPart, m))
|
||||||
output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d))
|
output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d))
|
||||||
}
|
}
|
||||||
} else if sc.TCP[srvPort] != nil {
|
} else if tcpHandler != nil {
|
||||||
h := sc.TCP[srvPort]
|
h := tcpHandler
|
||||||
|
|
||||||
tlsStatus := "TLS over TCP"
|
tlsStatus := "TLS over TCP"
|
||||||
if h.TerminateTLS != "" {
|
if h.TerminateTLS != "" {
|
||||||
tlsStatus = "TLS terminated"
|
tlsStatus = "TLS terminated"
|
||||||
}
|
}
|
||||||
|
|
||||||
output.WriteString(fmt.Sprintf("%s://%s%s\n", scheme, dnsName, portPart))
|
|
||||||
output.WriteString(fmt.Sprintf("|-- tcp://%s (%s)\n", hp, tlsStatus))
|
output.WriteString(fmt.Sprintf("|-- tcp://%s (%s)\n", hp, tlsStatus))
|
||||||
for _, a := range st.TailscaleIPs {
|
for _, a := range ips {
|
||||||
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort)))
|
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort)))
|
||||||
output.WriteString(fmt.Sprintf("|-- tcp://%s\n", ipp))
|
output.WriteString(fmt.Sprintf("|-- tcp://%s\n", ipp))
|
||||||
}
|
}
|
||||||
output.WriteString(fmt.Sprintf("|--> tcp://%s\n", h.TCPForward))
|
output.WriteString(fmt.Sprintf("|--> tcp://%s\n\n", h.TCPForward))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !e.bg {
|
if !forService && !e.bg.Value {
|
||||||
output.WriteString(msgToExit)
|
output.WriteString(msgToExit)
|
||||||
return output.String()
|
return output.String()
|
||||||
}
|
}
|
||||||
@ -479,14 +631,19 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
|||||||
|
|
||||||
output.WriteString(fmt.Sprintf(msgRunningInBackground, subCmdUpper))
|
output.WriteString(fmt.Sprintf(msgRunningInBackground, subCmdUpper))
|
||||||
output.WriteString("\n")
|
output.WriteString("\n")
|
||||||
output.WriteString(fmt.Sprintf(msgDisableProxy, subCmd, srvType.String(), srvPort))
|
if forService {
|
||||||
|
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()
|
return output.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target string) error {
|
func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16, useTLS bool, mount, target string) error {
|
||||||
h := new(ipn.HTTPHandler)
|
h := new(ipn.HTTPHandler)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(target, "text:"):
|
case strings.HasPrefix(target, "text:"):
|
||||||
text := strings.TrimPrefix(target, "text:")
|
text := strings.TrimPrefix(target, "text:")
|
||||||
@ -522,11 +679,11 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: validation needs to check nested foreground configs
|
// TODO: validation needs to check nested foreground configs
|
||||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
|
||||||
return errors.New("cannot serve web; already serving TCP")
|
return errors.New("cannot serve web; already serving TCP")
|
||||||
}
|
}
|
||||||
|
|
||||||
sc.SetWebHandler(h, dnsName, srvPort, mount, useTLS)
|
sc.SetWebHandler(st, h, dnsName, srvPort, mount, useTLS)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -553,7 +710,7 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: needs to account for multiple configs from foreground mode
|
// TODO: needs to account for multiple configs from foreground mode
|
||||||
if sc.IsServingWeb(srcPort) {
|
if sc.IsServingWeb(srcPort, dnsName) {
|
||||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,18 +735,24 @@ func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// unsetServe removes the serve config for the given serve port.
|
// unsetServe removes the serve config for the given serve port.
|
||||||
func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string) error {
|
// dnsName is a FQDN or a serviceName (with `svc:` prefix).
|
||||||
|
func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string) error {
|
||||||
switch srvType {
|
switch srvType {
|
||||||
case serveTypeHTTPS, serveTypeHTTP:
|
case serveTypeHTTPS, serveTypeHTTP:
|
||||||
err := e.removeWebServe(sc, dnsName, srvPort, mount)
|
err := e.removeWebServe(sc, st, dnsName, srvPort, mount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to remove web serve: %w", err)
|
return fmt.Errorf("failed to remove web serve: %w", err)
|
||||||
}
|
}
|
||||||
case serveTypeTCP, serveTypeTLSTerminatedTCP:
|
case serveTypeTCP, serveTypeTLSTerminatedTCP:
|
||||||
err := e.removeTCPServe(sc, srvPort)
|
err := e.removeTCPServe(sc, dnsName, srvPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to remove TCP serve: %w", err)
|
return fmt.Errorf("failed to remove TCP serve: %w", err)
|
||||||
}
|
}
|
||||||
|
case serveTypeTun:
|
||||||
|
err := e.removeTunServe(sc, dnsName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove TUN serve: %w", err)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid type %q", srvType)
|
return fmt.Errorf("invalid type %q", srvType)
|
||||||
}
|
}
|
||||||
@ -599,7 +762,7 @@ func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serve
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, err error) {
|
func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, wasDefault bool, err error) {
|
||||||
sourceMap := map[serveType]uint{
|
sourceMap := map[serveType]uint{
|
||||||
serveTypeHTTP: e.http,
|
serveTypeHTTP: e.http,
|
||||||
serveTypeHTTPS: e.https,
|
serveTypeHTTPS: e.https,
|
||||||
@ -612,22 +775,30 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, er
|
|||||||
for k, v := range sourceMap {
|
for k, v := range sourceMap {
|
||||||
if v != 0 {
|
if v != 0 {
|
||||||
if v > math.MaxUint16 {
|
if v > math.MaxUint16 {
|
||||||
return 0, 0, fmt.Errorf("port number %d is too high for %s flag", v, srvType)
|
return 0, 0, false, fmt.Errorf("port number %d is too high for %s flag", v, srvType)
|
||||||
}
|
}
|
||||||
srcTypeCount++
|
srcTypeCount++
|
||||||
srvType = k
|
srvType = k
|
||||||
srvPort = uint16(v)
|
srvPort = uint16(v)
|
||||||
|
wasDefault = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.tun {
|
||||||
|
srcTypeCount++
|
||||||
|
srvType = serveTypeTun
|
||||||
|
wasDefault = false
|
||||||
|
}
|
||||||
|
|
||||||
if srcTypeCount > 1 {
|
if srcTypeCount > 1 {
|
||||||
return 0, 0, fmt.Errorf("cannot serve multiple types for a single mount point")
|
return 0, 0, false, fmt.Errorf("cannot serve multiple types for a single mount point")
|
||||||
} else if srcTypeCount == 0 {
|
} else if srcTypeCount == 0 {
|
||||||
srvType = serveTypeHTTPS
|
srvType = serveTypeHTTPS
|
||||||
srvPort = 443
|
srvPort = 443
|
||||||
|
wasDefault = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return srvType, srvPort, nil
|
return srvType, srvPort, wasDefault, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isLegacyInvocation helps transition customers who have been using the beta
|
// isLegacyInvocation helps transition customers who have been using the beta
|
||||||
@ -728,27 +899,44 @@ func isLegacyInvocation(subcmd serveMode, args []string) (string, bool) {
|
|||||||
// and removes funnel if no remaining mounts exist for the serve port.
|
// and removes funnel if no remaining mounts exist for the serve port.
|
||||||
// The srvPort argument is the serving port and the mount argument is
|
// The srvPort argument is the serving port and the mount argument is
|
||||||
// the mount point or registered path to remove.
|
// the mount point or registered path to remove.
|
||||||
func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, mount string) error {
|
func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16, mount string) error {
|
||||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
if sc == nil {
|
||||||
return errors.New("cannot remove web handler; currently serving TCP")
|
return nil
|
||||||
|
}
|
||||||
|
forService := ipn.IsServiceName(dnsName)
|
||||||
|
portStr := strconv.Itoa(int(srvPort))
|
||||||
|
var hp ipn.HostPort
|
||||||
|
var webServeMap map[ipn.HostPort]*ipn.WebServerConfig
|
||||||
|
if forService {
|
||||||
|
svcName := tailcfg.ServiceName(dnsName)
|
||||||
|
dnsNameForService := svcName.WithoutPrefix() + "." + st.CurrentTailnet.MagicDNSSuffix
|
||||||
|
hp = ipn.HostPort(net.JoinHostPort(dnsNameForService, portStr))
|
||||||
|
if svc, ok := sc.Services[svcName]; !ok || svc == nil {
|
||||||
|
return errors.New("error: service does not exist")
|
||||||
|
} else {
|
||||||
|
webServeMap = svc.Web
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hp = ipn.HostPort(net.JoinHostPort(dnsName, portStr))
|
||||||
|
webServeMap = sc.Web
|
||||||
}
|
}
|
||||||
|
|
||||||
portStr := strconv.Itoa(int(srvPort))
|
if sc.IsTCPForwardingOnPort(srvPort, dnsName) {
|
||||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, portStr))
|
return errors.New("cannot remove web handler; currently serving TCP")
|
||||||
|
}
|
||||||
var targetExists bool
|
var targetExists bool
|
||||||
var mounts []string
|
var mounts []string
|
||||||
// mount is deduced from e.setPath but it is ambiguous as
|
// mount is deduced from e.setPath but it is ambiguous as
|
||||||
// to whether the user explicitly passed "/" or it was defaulted to.
|
// to whether the user explicitly passed "/" or it was defaulted to.
|
||||||
if e.setPath == "" {
|
if e.setPath == "" {
|
||||||
targetExists = sc.Web[hp] != nil && len(sc.Web[hp].Handlers) > 0
|
targetExists = webServeMap[hp] != nil && len(webServeMap[hp].Handlers) > 0
|
||||||
if targetExists {
|
if targetExists {
|
||||||
for mount := range sc.Web[hp].Handlers {
|
for mount := range webServeMap[hp].Handlers {
|
||||||
mounts = append(mounts, mount)
|
mounts = append(mounts, mount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetExists = sc.WebHandlerExists(hp, mount)
|
targetExists = sc.WebHandlerExists(dnsName, hp, mount)
|
||||||
mounts = []string{mount}
|
mounts = []string{mount}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -763,23 +951,46 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeTCPServe removes the TCP forwarding configuration for the
|
// removeTCPServe removes the TCP forwarding configuration for the
|
||||||
// given srvPort, or serving port.
|
// given srvPort, or serving port for the given dnsName.
|
||||||
func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error {
|
func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, dnsName string, src uint16) error {
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if sc.GetTCPPortHandler(src) == nil {
|
if sc.GetTCPPortHandler(src, dnsName) == nil {
|
||||||
return errors.New("error: serve config does not exist")
|
return errors.New("error: serve config does not exist")
|
||||||
}
|
}
|
||||||
if sc.IsServingWeb(src) {
|
if sc.IsServingWeb(src, dnsName) {
|
||||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
||||||
}
|
}
|
||||||
sc.RemoveTCPForwarding(src)
|
sc.RemoveTCPForwarding(dnsName, src)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *serveEnv) removeTunServe(sc *ipn.ServeConfig, dnsName string) error {
|
||||||
|
if sc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
svcName := tailcfg.ServiceName(dnsName)
|
||||||
|
svc, ok := sc.Services[svcName]
|
||||||
|
if !ok || svc == nil {
|
||||||
|
return errors.New("error: service does not exist")
|
||||||
|
}
|
||||||
|
if !svc.Tun {
|
||||||
|
return errors.New("error: service is not being served in TUN mode")
|
||||||
|
}
|
||||||
|
delete(sc.Services, svcName)
|
||||||
|
if len(sc.Services) == 0 {
|
||||||
|
sc.Services = nil // clean up empty map
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServeDevConfigMutations(t *testing.T) {
|
func TestServeDevConfigMutations(t *testing.T) {
|
||||||
@ -874,15 +876,17 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
desc string
|
desc string
|
||||||
cfg *ipn.ServeConfig
|
cfg *ipn.ServeConfig
|
||||||
|
dns string
|
||||||
servePort uint16
|
servePort uint16
|
||||||
serveType serveType
|
serveType serveType
|
||||||
bg bool
|
bg bgBoolFlag
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "nil_config",
|
name: "nil_config",
|
||||||
desc: "when config is nil, all requests valid",
|
desc: "when config is nil, all requests valid",
|
||||||
cfg: nil,
|
cfg: nil,
|
||||||
|
dns: "node.test.ts.net",
|
||||||
servePort: 3000,
|
servePort: 3000,
|
||||||
serveType: serveTypeHTTPS,
|
serveType: serveTypeHTTPS,
|
||||||
},
|
},
|
||||||
@ -894,7 +898,8 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
443: {HTTPS: true},
|
443: {HTTPS: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bg: true,
|
dns: "node.test.ts.net",
|
||||||
|
bg: bgBoolFlag{true, false},
|
||||||
servePort: 10000,
|
servePort: 10000,
|
||||||
serveType: serveTypeHTTPS,
|
serveType: serveTypeHTTPS,
|
||||||
},
|
},
|
||||||
@ -906,7 +911,8 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
443: {TCPForward: "http://localhost:4545"},
|
443: {TCPForward: "http://localhost:4545"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bg: true,
|
dns: "node.test.ts.net",
|
||||||
|
bg: bgBoolFlag{true, false},
|
||||||
servePort: 443,
|
servePort: 443,
|
||||||
serveType: serveTypeTCP,
|
serveType: serveTypeTCP,
|
||||||
},
|
},
|
||||||
@ -918,7 +924,8 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
443: {HTTPS: true},
|
443: {HTTPS: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bg: true,
|
dns: "node.test.ts.net",
|
||||||
|
bg: bgBoolFlag{true, false},
|
||||||
servePort: 443,
|
servePort: 443,
|
||||||
serveType: serveTypeHTTP,
|
serveType: serveTypeHTTP,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
@ -938,6 +945,7 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dns: "node.test.ts.net",
|
||||||
servePort: 4040,
|
servePort: 4040,
|
||||||
serveType: serveTypeTCP,
|
serveType: serveTypeTCP,
|
||||||
},
|
},
|
||||||
@ -953,16 +961,95 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dns: "node.test.ts.net",
|
||||||
servePort: 3000,
|
servePort: 3000,
|
||||||
serveType: serveTypeTCP,
|
serveType: serveTypeTCP,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "new_service_tcp",
|
||||||
|
desc: "no error when adding a new service port",
|
||||||
|
cfg: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "svc:foo",
|
||||||
|
servePort: 8080,
|
||||||
|
serveType: serveTypeTCP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override_service_tcp",
|
||||||
|
desc: "no error when overwriting a previous service port",
|
||||||
|
cfg: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {TCPForward: "http://localhost:4545"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "svc:foo",
|
||||||
|
servePort: 443,
|
||||||
|
serveType: serveTypeTCP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override_service_tcp",
|
||||||
|
desc: "error when overwriting a previous service port with a different serve type",
|
||||||
|
cfg: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {HTTPS: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "svc:foo",
|
||||||
|
servePort: 443,
|
||||||
|
serveType: serveTypeHTTP,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override_service_tcp",
|
||||||
|
desc: "error when setting previous tcp service to tun mode",
|
||||||
|
cfg: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {TCPForward: "http://localhost:4545"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "svc:foo",
|
||||||
|
serveType: serveTypeTun,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override_service_tun",
|
||||||
|
desc: "error when setting previous tun service to tcp forwarder",
|
||||||
|
cfg: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
Tun: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "svc:foo",
|
||||||
|
serveType: serveTypeTCP,
|
||||||
|
servePort: 443,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
se := serveEnv{bg: tc.bg}
|
se := serveEnv{bg: tc.bg}
|
||||||
err := se.validateConfig(tc.cfg, tc.servePort, tc.serveType)
|
err := se.validateConfig(tc.cfg, tc.servePort, tc.serveType, tc.dns)
|
||||||
if err == nil && tc.wantErr {
|
if err == nil && tc.wantErr {
|
||||||
t.Fatal("expected an error but got nil")
|
t.Fatal("expected an error but got nil")
|
||||||
}
|
}
|
||||||
@ -976,58 +1063,65 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
|
|
||||||
func TestSrcTypeFromFlags(t *testing.T) {
|
func TestSrcTypeFromFlags(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
env *serveEnv
|
env *serveEnv
|
||||||
expectedType serveType
|
expectedType serveType
|
||||||
expectedPort uint16
|
expectedPort uint16
|
||||||
expectedErr bool
|
expectedErr bool
|
||||||
|
expectedWasDefault bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "only http set",
|
name: "only http set",
|
||||||
env: &serveEnv{http: 80},
|
env: &serveEnv{http: 80},
|
||||||
expectedType: serveTypeHTTP,
|
expectedType: serveTypeHTTP,
|
||||||
expectedPort: 80,
|
expectedPort: 80,
|
||||||
expectedErr: false,
|
expectedErr: false,
|
||||||
|
expectedWasDefault: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only https set",
|
name: "only https set",
|
||||||
env: &serveEnv{https: 10000},
|
env: &serveEnv{https: 10000},
|
||||||
expectedType: serveTypeHTTPS,
|
expectedType: serveTypeHTTPS,
|
||||||
expectedPort: 10000,
|
expectedPort: 10000,
|
||||||
expectedErr: false,
|
expectedErr: false,
|
||||||
|
expectedWasDefault: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only tcp set",
|
name: "only tcp set",
|
||||||
env: &serveEnv{tcp: 8000},
|
env: &serveEnv{tcp: 8000},
|
||||||
expectedType: serveTypeTCP,
|
expectedType: serveTypeTCP,
|
||||||
expectedPort: 8000,
|
expectedPort: 8000,
|
||||||
expectedErr: false,
|
expectedErr: false,
|
||||||
|
expectedWasDefault: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only tls-terminated-tcp set",
|
name: "only tls-terminated-tcp set",
|
||||||
env: &serveEnv{tlsTerminatedTCP: 8080},
|
env: &serveEnv{tlsTerminatedTCP: 8080},
|
||||||
expectedType: serveTypeTLSTerminatedTCP,
|
expectedType: serveTypeTLSTerminatedTCP,
|
||||||
expectedPort: 8080,
|
expectedPort: 8080,
|
||||||
expectedErr: false,
|
expectedErr: false,
|
||||||
|
expectedWasDefault: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "defaults to https, port 443",
|
name: "defaults to https, port 443",
|
||||||
env: &serveEnv{},
|
env: &serveEnv{},
|
||||||
expectedType: serveTypeHTTPS,
|
expectedType: serveTypeHTTPS,
|
||||||
expectedPort: 443,
|
expectedPort: 443,
|
||||||
expectedErr: false,
|
expectedErr: false,
|
||||||
|
expectedWasDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple types set",
|
name: "multiple types set",
|
||||||
env: &serveEnv{http: 80, https: 443},
|
env: &serveEnv{http: 80, https: 443},
|
||||||
expectedPort: 0,
|
expectedPort: 0,
|
||||||
expectedErr: true,
|
expectedErr: true,
|
||||||
|
expectedWasDefault: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
srcType, srcPort, err := srvTypeAndPortFromFlags(tt.env)
|
srcType, srcPort, explicitSet, err := srvTypeAndPortFromFlags(tt.env)
|
||||||
if (err != nil) != tt.expectedErr {
|
if (err != nil) != tt.expectedErr {
|
||||||
t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err)
|
t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err)
|
||||||
}
|
}
|
||||||
@ -1037,6 +1131,9 @@ func TestSrcTypeFromFlags(t *testing.T) {
|
|||||||
if srcPort != tt.expectedPort {
|
if srcPort != tt.expectedPort {
|
||||||
t.Errorf("Expected srcPort: %d, got: %d", tt.expectedPort, srcPort)
|
t.Errorf("Expected srcPort: %d, got: %d", tt.expectedPort, srcPort)
|
||||||
}
|
}
|
||||||
|
if explicitSet != tt.expectedWasDefault {
|
||||||
|
t.Errorf("Expected defaultFlag: %v, got: %v", tt.expectedWasDefault, explicitSet)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1076,11 +1173,21 @@ func TestCleanURLPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMessageForPort(t *testing.T) {
|
func TestMessageForPort(t *testing.T) {
|
||||||
|
svcIPMap := tailcfg.ServiceIPMappings{
|
||||||
|
"svc:foo": []netip.Addr{
|
||||||
|
netip.MustParseAddr("100.101.101.101"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:6565:6565"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svcIPMapJSON, _ := json.Marshal(svcIPMap)
|
||||||
|
svcIPMapJSONRawMSG := tailcfg.RawMessage(svcIPMapJSON)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
subcmd serveMode
|
subcmd serveMode
|
||||||
serveConfig *ipn.ServeConfig
|
serveConfig *ipn.ServeConfig
|
||||||
status *ipnstate.Status
|
status *ipnstate.Status
|
||||||
|
prefs *ipn.Prefs
|
||||||
dnsName string
|
dnsName string
|
||||||
srvType serveType
|
srvType serveType
|
||||||
srvPort uint16
|
srvPort uint16
|
||||||
@ -1147,10 +1254,206 @@ func TestMessageForPort(t *testing.T) {
|
|||||||
fmt.Sprintf(msgDisableProxy, "serve", "http", 80),
|
fmt.Sprintf(msgDisableProxy, "serve", "http", 80),
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "serve service http",
|
||||||
|
subcmd: serve,
|
||||||
|
serveConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
},
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"foo.test.ts.net:80": {
|
||||||
|
Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Proxy: "http://localhost:3000"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
Self: &ipnstate.PeerStatus{
|
||||||
|
CapMap: tailcfg.NodeCapMap{
|
||||||
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prefs: &ipn.Prefs{
|
||||||
|
AdvertiseServices: []string{"svc:foo"},
|
||||||
|
},
|
||||||
|
dnsName: "svc:foo",
|
||||||
|
srvType: serveTypeHTTP,
|
||||||
|
srvPort: 80,
|
||||||
|
expected: strings.Join([]string{
|
||||||
|
msgServeAvailable,
|
||||||
|
"",
|
||||||
|
"http://foo.test.ts.net/",
|
||||||
|
"|-- proxy http://localhost:3000",
|
||||||
|
"",
|
||||||
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
||||||
|
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "http", 80),
|
||||||
|
fmt.Sprintf(msgDisableService, "svc:foo"),
|
||||||
|
}, "\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "serve service no capmap",
|
||||||
|
subcmd: serve,
|
||||||
|
serveConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:bar": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
},
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"bar.test.ts.net:80": {
|
||||||
|
Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Proxy: "http://localhost:3000"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
Self: &ipnstate.PeerStatus{
|
||||||
|
CapMap: tailcfg.NodeCapMap{
|
||||||
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prefs: &ipn.Prefs{
|
||||||
|
AdvertiseServices: []string{"svc:bar"},
|
||||||
|
},
|
||||||
|
dnsName: "svc:bar",
|
||||||
|
srvType: serveTypeHTTP,
|
||||||
|
srvPort: 80,
|
||||||
|
expected: strings.Join([]string{
|
||||||
|
msgServiceIPNotAssigned,
|
||||||
|
"",
|
||||||
|
"http://bar.test.ts.net/",
|
||||||
|
"|-- proxy http://localhost:3000",
|
||||||
|
"",
|
||||||
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
||||||
|
fmt.Sprintf(msgDisableServiceProxy, "svc:bar", "http", 80),
|
||||||
|
fmt.Sprintf(msgDisableService, "svc:bar"),
|
||||||
|
}, "\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "serve service https non-default port",
|
||||||
|
subcmd: serve,
|
||||||
|
serveConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
2200: {HTTPS: true},
|
||||||
|
},
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"foo.test.ts.net:2200": {
|
||||||
|
Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Proxy: "http://localhost:3000"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
Self: &ipnstate.PeerStatus{
|
||||||
|
CapMap: tailcfg.NodeCapMap{
|
||||||
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
|
||||||
|
dnsName: "svc:foo",
|
||||||
|
srvType: serveTypeHTTPS,
|
||||||
|
srvPort: 2200,
|
||||||
|
expected: strings.Join([]string{
|
||||||
|
msgServeAvailable,
|
||||||
|
"",
|
||||||
|
"https://foo.test.ts.net:2200/",
|
||||||
|
"|-- proxy http://localhost:3000",
|
||||||
|
"",
|
||||||
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
||||||
|
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "https", 2200),
|
||||||
|
fmt.Sprintf(msgDisableService, "svc:foo"),
|
||||||
|
}, "\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "serve service TCPForward",
|
||||||
|
subcmd: serve,
|
||||||
|
serveConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
2200: {TCPForward: "localhost:3000"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
Self: &ipnstate.PeerStatus{
|
||||||
|
CapMap: tailcfg.NodeCapMap{
|
||||||
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
|
||||||
|
dnsName: "svc:foo",
|
||||||
|
srvType: serveTypeTCP,
|
||||||
|
srvPort: 2200,
|
||||||
|
expected: strings.Join([]string{
|
||||||
|
msgServeAvailable,
|
||||||
|
"",
|
||||||
|
"|-- tcp://foo.test.ts.net:2200 (TLS over TCP)",
|
||||||
|
"|-- tcp://100.101.101.101:2200",
|
||||||
|
"|-- tcp://[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:2200",
|
||||||
|
"|--> tcp://localhost:3000",
|
||||||
|
"",
|
||||||
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
||||||
|
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "tcp", 2200),
|
||||||
|
fmt.Sprintf(msgDisableService, "svc:foo"),
|
||||||
|
}, "\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "serve service Tun",
|
||||||
|
subcmd: serve,
|
||||||
|
serveConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
Tun: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
Self: &ipnstate.PeerStatus{
|
||||||
|
CapMap: tailcfg.NodeCapMap{
|
||||||
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
|
||||||
|
dnsName: "svc:foo",
|
||||||
|
srvType: serveTypeTun,
|
||||||
|
expected: strings.Join([]string{
|
||||||
|
msgServeAvailable,
|
||||||
|
"",
|
||||||
|
fmt.Sprintf(msgRunningTunServie, "foo.test.ts.net"),
|
||||||
|
fmt.Sprintf(msgDisableServiceTun, "svc:foo"),
|
||||||
|
fmt.Sprintf(msgDisableService, "svc:foo"),
|
||||||
|
}, "\n"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
e := &serveEnv{bg: true, subcmd: tt.subcmd}
|
e := &serveEnv{bg: bgBoolFlag{true, false}, subcmd: tt.subcmd}
|
||||||
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
actual := e.messageForPort(tt.serveConfig, tt.status, tt.dnsName, tt.srvType, tt.srvPort)
|
actual := e.messageForPort(tt.serveConfig, tt.status, tt.dnsName, tt.srvType, tt.srvPort)
|
||||||
@ -1277,6 +1580,477 @@ 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnsetServe(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
cfg *ipn.ServeConfig
|
||||||
|
st *ipnstate.Status
|
||||||
|
dnsName string
|
||||||
|
srvType serveType
|
||||||
|
srvPort uint16
|
||||||
|
mount string
|
||||||
|
setServeEnv bool
|
||||||
|
serveEnv *serveEnv // if set, use this instead of the default serveEnv
|
||||||
|
expected *ipn.ServeConfig
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unset http handler",
|
||||||
|
desc: "remove an existing http handler",
|
||||||
|
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,
|
||||||
|
mount: "/",
|
||||||
|
expected: &ipn.ServeConfig{},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset service handler",
|
||||||
|
desc: "remove an existing service TCP handler",
|
||||||
|
cfg: &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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
st: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
},
|
||||||
|
dnsName: "svc:bar",
|
||||||
|
srvType: serveTypeHTTP,
|
||||||
|
srvPort: 80,
|
||||||
|
mount: "/",
|
||||||
|
expected: &ipn.ServeConfig{},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset service handler tun",
|
||||||
|
desc: "remove an existing service handler in tun mode",
|
||||||
|
cfg: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:bar": {
|
||||||
|
Tun: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
st: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
},
|
||||||
|
dnsName: "svc:bar",
|
||||||
|
srvType: serveTypeTun,
|
||||||
|
expected: &ipn.ServeConfig{},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset service handler tcp",
|
||||||
|
desc: "remove an existing service TCP handler",
|
||||||
|
cfg: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:bar": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {TCPForward: "11.11.11.11:3000"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
st: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
},
|
||||||
|
dnsName: "svc:bar",
|
||||||
|
srvType: serveTypeTCP,
|
||||||
|
srvPort: 80,
|
||||||
|
expected: &ipn.ServeConfig{},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset http handler not found",
|
||||||
|
desc: "try to remove a non-existing http handler",
|
||||||
|
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: "bar.test.ts.net",
|
||||||
|
srvType: serveTypeHTTP,
|
||||||
|
srvPort: 80,
|
||||||
|
mount: "/abc",
|
||||||
|
expected: &ipn.ServeConfig{},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset service handler not found",
|
||||||
|
desc: "try to remove a non-existing service TCP handler",
|
||||||
|
|
||||||
|
cfg: &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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
st: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
},
|
||||||
|
dnsName: "svc:bar",
|
||||||
|
srvType: serveTypeHTTP,
|
||||||
|
srvPort: 80,
|
||||||
|
mount: "/abc",
|
||||||
|
setServeEnv: true,
|
||||||
|
serveEnv: &serveEnv{setPath: "/abc"},
|
||||||
|
expected: &ipn.ServeConfig{},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset service doesn't exist",
|
||||||
|
desc: "try to remove a non-existing service's handler",
|
||||||
|
cfg: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:bar": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {TCPForward: "11.11.11.11:3000"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
st: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
},
|
||||||
|
dnsName: "svc:foo",
|
||||||
|
srvType: serveTypeTCP,
|
||||||
|
srvPort: 80,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset tcp while port is in use",
|
||||||
|
desc: "try to remove a TCP handler while the port is used for web",
|
||||||
|
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: serveTypeTCP,
|
||||||
|
srvPort: 80,
|
||||||
|
mount: "/",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset service tcp while port is in use",
|
||||||
|
desc: "try to remove a service TCP handler while the port is used for web",
|
||||||
|
cfg: &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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
st: &ipnstate.Status{
|
||||||
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||||
|
},
|
||||||
|
dnsName: "svc:bar",
|
||||||
|
srvType: serveTypeTCP,
|
||||||
|
srvPort: 80,
|
||||||
|
mount: "/",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
e := &serveEnv{}
|
||||||
|
if tt.setServeEnv {
|
||||||
|
e = tt.serveEnv
|
||||||
|
}
|
||||||
|
err := e.unsetServe(tt.cfg, tt.st, tt.dnsName, tt.srvType, tt.srvPort, tt.mount)
|
||||||
|
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.Fatalf("got: %v; expected: %v", tt.cfg, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// exactErrMsg returns an error checker that wants exactly the provided want error.
|
// exactErrMsg returns an error checker that wants exactly the provided want error.
|
||||||
// If optName is non-empty, it's used in the error message.
|
// If optName is non-empty, it's used in the error message.
|
||||||
func exactErrMsg(want error) func(error) string {
|
func exactErrMsg(want error) func(error) string {
|
||||||
|
@ -260,7 +260,7 @@ func printFunnelStatus(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
sni, portStr, _ := net.SplitHostPort(string(hp))
|
sni, portStr, _ := net.SplitHostPort(string(hp))
|
||||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||||
isTCP := sc.IsTCPForwardingOnPort(uint16(p))
|
isTCP := sc.IsTCPForwardingOnPort(uint16(p), sni)
|
||||||
url := "https://"
|
url := "https://"
|
||||||
if isTCP {
|
if isTCP {
|
||||||
url = "tcp://"
|
url = "tcp://"
|
||||||
|
@ -266,7 +266,7 @@ func serveOnLocalTailscaled(ctx context.Context, lc *local.Client, st *ipnstate.
|
|||||||
fmt.Printf("setting funnel for %s:%v\n", serverURL, dstPort)
|
fmt.Printf("setting funnel for %s:%v\n", serverURL, dstPort)
|
||||||
|
|
||||||
foregroundSc.SetFunnel(serverURL, dstPort, shouldFunnel)
|
foregroundSc.SetFunnel(serverURL, dstPort, shouldFunnel)
|
||||||
foregroundSc.SetWebHandler(&ipn.HTTPHandler{
|
foregroundSc.SetWebHandler(st, &ipn.HTTPHandler{
|
||||||
Proxy: fmt.Sprintf("https://%s", net.JoinHostPort(serverURL, strconv.Itoa(int(dstPort)))),
|
Proxy: fmt.Sprintf("https://%s", net.JoinHostPort(serverURL, strconv.Itoa(int(dstPort)))),
|
||||||
}, serverURL, uint16(*flagPort), "/", true)
|
}, serverURL, uint16(*flagPort), "/", true)
|
||||||
err = lc.SetServeConfig(ctx, sc)
|
err = lc.SetServeConfig(ctx, sc)
|
||||||
|
214
ipn/serve.go
214
ipn/serve.go
@ -166,15 +166,26 @@ type HTTPHandler struct {
|
|||||||
|
|
||||||
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
|
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
|
||||||
// the given host:port and mount point.
|
// the given host:port and mount point.
|
||||||
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
|
func (sc *ServeConfig) WebHandlerExists(dnsName string, hp HostPort, mount string) bool {
|
||||||
h := sc.GetWebHandler(hp, mount)
|
h := sc.GetWebHandler(dnsName, hp, mount)
|
||||||
return h != nil
|
return h != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebHandler returns the HTTPHandler for the given host:port and mount point.
|
// GetWebHandler returns the HTTPHandler for the given host:port and mount point.
|
||||||
// Returns nil if the handler does not exist.
|
// Returns nil if the handler does not exist.
|
||||||
func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler {
|
func (sc *ServeConfig) GetWebHandler(dnsName string, hp HostPort, mount string) *HTTPHandler {
|
||||||
if sc == nil || sc.Web[hp] == nil {
|
if sc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if IsServiceName(dnsName) {
|
||||||
|
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc.Web != nil {
|
||||||
|
if webCfg, ok := svc.Web[hp]; ok {
|
||||||
|
return webCfg.Handlers[mount]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if sc.Web[hp] == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return sc.Web[hp].Handlers[mount]
|
return sc.Web[hp].Handlers[mount]
|
||||||
@ -182,10 +193,16 @@ func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler {
|
|||||||
|
|
||||||
// GetTCPPortHandler returns the TCPPortHandler for the given port.
|
// GetTCPPortHandler returns the TCPPortHandler for the given port.
|
||||||
// If the port is not configured, nil is returned.
|
// If the port is not configured, nil is returned.
|
||||||
func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
|
func (sc *ServeConfig) GetTCPPortHandler(port uint16, dnsName string) *TCPPortHandler {
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if IsServiceName(dnsName) {
|
||||||
|
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc != nil {
|
||||||
|
return svc.TCP[port]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return sc.TCP[port]
|
return sc.TCP[port]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,34 +244,78 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding
|
// IsServiceName reports whether if the given string is a valid service name.
|
||||||
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving.
|
func IsServiceName(s string) bool {
|
||||||
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
|
return tailcfg.ServiceName(s).Validate() == nil
|
||||||
if sc == nil || sc.TCP[port] == nil {
|
}
|
||||||
|
|
||||||
|
// IsTCPForwardingOnPort reports whether ServeConfig is currently forwarding
|
||||||
|
// in TCPForward mode on the given port for a DNSName. DNSName will be either node's DNSName, or a
|
||||||
|
// serviceName for service hosted on node. This is exclusive of Web/HTTPS serving.
|
||||||
|
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16, dnsName string) bool {
|
||||||
|
if sc == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return !sc.IsServingWeb(port)
|
forService := IsServiceName(dnsName)
|
||||||
|
if forService {
|
||||||
|
svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]
|
||||||
|
if !ok || svc == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if svc.TCP[port] == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if sc.TCP[port] == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !sc.IsServingWeb(port, dnsName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsServingWeb reports whether if ServeConfig is currently serving Web
|
// IsServingWeb reports whether ServeConfig is currently serving Web
|
||||||
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding.
|
// (HTTP/HTTPS) on the given port for a DNSName. DNSName will be either node's DNSName, or a
|
||||||
func (sc *ServeConfig) IsServingWeb(port uint16) bool {
|
// serviceName for service hosted on node. This is exclusive of TCPForwarding.
|
||||||
return sc.IsServingHTTP(port) || sc.IsServingHTTPS(port)
|
func (sc *ServeConfig) IsServingWeb(port uint16, dnsName string) bool {
|
||||||
|
return sc.IsServingHTTP(port, dnsName) || sc.IsServingHTTPS(port, dnsName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsServingHTTPS reports whether if ServeConfig is currently serving HTTPS on
|
// IsServingHTTPS reports whether ServeConfig is currently serving HTTPS on
|
||||||
// the given port. This is exclusive of HTTP and TCPForwarding.
|
// the given port for a DNSName. DNSName will be either node's DNSName, or a
|
||||||
func (sc *ServeConfig) IsServingHTTPS(port uint16) bool {
|
// serviceName for service hosted on node. This is exclusive of HTTP and TCPForwarding.
|
||||||
if sc == nil || sc.TCP[port] == nil {
|
func (sc *ServeConfig) IsServingHTTPS(port uint16, dnsName string) bool {
|
||||||
|
if sc == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if IsServiceName(dnsName) {
|
||||||
|
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc != nil {
|
||||||
|
if svc.TCP[port] != nil {
|
||||||
|
return svc.TCP[port].HTTPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if sc.TCP[port] == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return sc.TCP[port].HTTPS
|
return sc.TCP[port].HTTPS
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the
|
// IsServingHTTP reports whether ServeConfig is currently serving HTTP on the
|
||||||
// given port. This is exclusive of HTTPS and TCPForwarding.
|
// given port for a DNSName. DNSName will be either node's DNSName, or a
|
||||||
func (sc *ServeConfig) IsServingHTTP(port uint16) bool {
|
// serviceName for service hosted on node. This is exclusive of HTTPS and TCPForwarding.
|
||||||
if sc == nil || sc.TCP[port] == nil {
|
func (sc *ServeConfig) IsServingHTTP(port uint16, dnsName string) bool {
|
||||||
|
if sc == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if IsServiceName(dnsName) {
|
||||||
|
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc != nil {
|
||||||
|
if svc.TCP[port] != nil {
|
||||||
|
return svc.TCP[port].HTTP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if sc.TCP[port] == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return sc.TCP[port].HTTP
|
return sc.TCP[port].HTTP
|
||||||
@ -278,23 +339,51 @@ func (sc *ServeConfig) FindConfig(port uint16) (*ServeConfig, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindServiceConfig finds the ServiceConfig for the given service name when it
|
||||||
|
// is hosting the given port.
|
||||||
|
func (sc *ServeConfig) FindServiceConfig(svcName tailcfg.ServiceName) *ServiceConfig {
|
||||||
|
if sc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if svc, ok := sc.Services[svcName]; ok && svc != nil {
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetWebHandler sets the given HTTPHandler at the specified host, port,
|
// SetWebHandler sets the given HTTPHandler at the specified host, port,
|
||||||
// and mount in the serve config. sc.TCP is also updated to reflect web
|
// and mount in the serve config. sc.TCP is also updated to reflect web
|
||||||
// serving usage of the given port.
|
// serving usage of the given port.
|
||||||
func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uint16, mount string, useTLS bool) {
|
func (sc *ServeConfig) SetWebHandler(st *ipnstate.Status, handler *HTTPHandler, host string, port uint16, mount string, useTLS bool) {
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
sc = new(ServeConfig)
|
sc = new(ServeConfig)
|
||||||
}
|
}
|
||||||
mak.Set(&sc.TCP, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
|
var hp HostPort
|
||||||
|
var webCfg *WebServerConfig
|
||||||
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
|
if IsServiceName(host) {
|
||||||
if _, ok := sc.Web[hp]; !ok {
|
svcName := tailcfg.ServiceName(host)
|
||||||
mak.Set(&sc.Web, hp, new(WebServerConfig))
|
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
|
// TODO(tylersmalley): handle multiple web handlers from foreground mode
|
||||||
for k, v := range sc.Web[hp].Handlers {
|
for k, v := range webCfg.Handlers {
|
||||||
if v == handler {
|
if v == handler {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -305,7 +394,7 @@ func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uin
|
|||||||
m1 := strings.TrimSuffix(mount, "/")
|
m1 := strings.TrimSuffix(mount, "/")
|
||||||
m2 := strings.TrimSuffix(k, "/")
|
m2 := strings.TrimSuffix(k, "/")
|
||||||
if m1 == m2 {
|
if m1 == m2 {
|
||||||
delete(sc.Web[hp].Handlers, k)
|
delete(webCfg.Handlers, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -318,9 +407,20 @@ func (sc *ServeConfig) SetTCPForwarding(port uint16, fwdAddr string, terminateTL
|
|||||||
if sc == nil {
|
if sc == nil {
|
||||||
sc = new(ServeConfig)
|
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 {
|
if terminateTLS {
|
||||||
sc.TCP[port].TerminateTLS = host
|
tcpPortHandler.TerminateTLS = host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,7 +445,7 @@ func (sc *ServeConfig) SetFunnel(host string, port uint16, setOn bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveWebHandler deletes the web handlers at all of the given mount points
|
// RemoveWebHandler deletes the web handlers at all of the given mount points
|
||||||
// for the provided host and port in the serve config. If cleanupFunnel is
|
// for the provided host and port in the serve config for node. If cleanupFunnel is
|
||||||
// true, this also removes the funnel value for this port if no handlers remain.
|
// true, this also removes the funnel value for this port if no handlers remain.
|
||||||
func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []string, cleanupFunnel bool) {
|
func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []string, cleanupFunnel bool) {
|
||||||
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
|
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
|
||||||
@ -374,9 +474,51 @@ 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)
|
||||||
|
}
|
||||||
|
if len(svc.Web) == 0 && len(svc.TCP) == 0 {
|
||||||
|
delete(sc.Services, svcName)
|
||||||
|
}
|
||||||
|
if len(sc.Services) == 0 {
|
||||||
|
sc.Services = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveTCPForwarding deletes the TCP forwarding configuration for the given
|
// RemoveTCPForwarding deletes the TCP forwarding configuration for the given
|
||||||
// port from the serve config.
|
// port from the serve config.
|
||||||
func (sc *ServeConfig) RemoveTCPForwarding(port uint16) {
|
func (sc *ServeConfig) RemoveTCPForwarding(dnsName string, port uint16) {
|
||||||
|
if IsServiceName(dnsName) {
|
||||||
|
if svc, ok := sc.Services[tailcfg.ServiceName(dnsName)]; ok && svc != nil {
|
||||||
|
delete(svc.TCP, port)
|
||||||
|
if len(svc.TCP) == 0 {
|
||||||
|
svc.TCP = nil
|
||||||
|
}
|
||||||
|
if len(svc.Web) == 0 && len(svc.TCP) == 0 {
|
||||||
|
delete(sc.Services, tailcfg.ServiceName(dnsName))
|
||||||
|
}
|
||||||
|
if len(sc.Services) == 0 {
|
||||||
|
sc.Services = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
delete(sc.TCP, port)
|
delete(sc.TCP, port)
|
||||||
if len(sc.TCP) == 0 {
|
if len(sc.TCP) == 0 {
|
||||||
sc.TCP = nil
|
sc.TCP = nil
|
||||||
|
@ -128,6 +128,121 @@ func TestHasPathHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsTCPForwardingOnPort(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg ServeConfig
|
||||||
|
dns string
|
||||||
|
port uint16
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty-config",
|
||||||
|
cfg: ServeConfig{},
|
||||||
|
dns: "foo.test.ts.net",
|
||||||
|
port: 80,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node-tcp-config-match",
|
||||||
|
cfg: ServeConfig{
|
||||||
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
||||||
|
},
|
||||||
|
dns: "foo.test.ts.net",
|
||||||
|
port: 80,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node-tcp-config-no-match",
|
||||||
|
cfg: ServeConfig{
|
||||||
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
||||||
|
},
|
||||||
|
dns: "foo.test.ts.net",
|
||||||
|
port: 443,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node-tcp-config-no-match-with-service",
|
||||||
|
cfg: ServeConfig{
|
||||||
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
||||||
|
},
|
||||||
|
dns: "svc:bar",
|
||||||
|
port: 80,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node-web-config-no-match",
|
||||||
|
cfg: ServeConfig{
|
||||||
|
TCP: map[uint16]*TCPPortHandler{80: {HTTPS: true}},
|
||||||
|
Web: map[HostPort]*WebServerConfig{
|
||||||
|
"foo.test.ts.net:80": {
|
||||||
|
Handlers: map[string]*HTTPHandler{
|
||||||
|
"/": {Text: "Hello, world!"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "foo.test.ts.net",
|
||||||
|
port: 80,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service-tcp-config-match",
|
||||||
|
cfg: ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "svc:foo",
|
||||||
|
port: 80,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service-tcp-config-no-match",
|
||||||
|
cfg: ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "svc:bar",
|
||||||
|
port: 80,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service-web-config-no-match",
|
||||||
|
cfg: ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*TCPPortHandler{80: {HTTPS: true}},
|
||||||
|
Web: map[HostPort]*WebServerConfig{
|
||||||
|
"foo.test.ts.net:80": {
|
||||||
|
Handlers: map[string]*HTTPHandler{
|
||||||
|
"/": {Text: "Hello, world!"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dns: "svc:foo",
|
||||||
|
port: 80,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.cfg.IsTCPForwardingOnPort(tt.port, tt.dns)
|
||||||
|
if tt.want != got {
|
||||||
|
t.Errorf("IsTCPForwardingOnPort() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExpandProxyTargetDev(t *testing.T) {
|
func TestExpandProxyTargetDev(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
Loading…
x
Reference in New Issue
Block a user