mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-13 14:43:19 +00:00
cmd/tailscale/cli: Add service flag to serve command (#16191)
* 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> * cmd/tailscale/cli: fix ai comments and fix a test Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Add a test for addServiceToPrefs Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: fix comment Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * add dnsName in error message Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * change the cli input flag variable type Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace FindServiceConfig with map lookup Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * some code simplification and add asServiceName This commit cotains code simplification for IsServingHTTPS, SetWebHandler, SetTCPForwarding Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace IsServiceName with tailcfg.AsServiceName Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace all assemble of host name for service with strings.Join Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: adjust parameter order and update output message This commit updates the parameter order for IsTCPForwardingOnPort and SetWebHandler. Also updated the message msgServiceIPNotAssigned to msgServiceWaitingApproval to adapt to latest terminologies around services. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: flip bool condition This commit fixes a previous bug added that throws error when serve funnel without service. It should've been the opposite, which throws error when serve funnel with service. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: change parameter of IsTCPForwardingOnPort This commit changes the dnsName string parameter for IsTCPForwardingOnPort to svcName tailcfg.ServiceName. This change is made to reduce ambiguity when a single service might have different dnsNames Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * ipn/ipnlocal: replace the key to webHandler for services This commit changes the way we get the webhandler for vipServices. It used to use the host name from request to find the webHandler, now everything targeting the vipService IP have the same set of handlers. This commit also stores service:port instead of FQDN:port as the key in serviceConfig for Web map. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Updated use of service name. This commit removes serviceName.IsEmpty and use direct comparison to instead. In legacy code, when an empty service name needs to be passed, a new constant noService is passed. Removed redundant code for checking service name validity and string method for serviceNameFlag. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Update bgBoolFlag This commit update field name, set and string method of bgBoolFlag to make code cleaner. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: remove isDefaultService output from srvTypeAndPortFromFlags This commit removes the isDefaultService out put as it's no longer needed. Also deleted redundant code. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: remove unnessesary variable declare in messageForPort Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace bool output for AsServiceName with err Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Replace DNSName with NoService if DNSname only used to identify service This commit moves noService constant to tailcfg, updates AsServiceName to return tailcfg.NoService if the input is not a valid service name. This commit also removes using the local DNSName as scvName parameter. When a function is only using DNSName to identify if it's working with a service, the input in replaced with svcName and expect caller to pass tailcfg.NoService if it's a local serve. This commit also replaces some use of Sprintf with net.JoinHostPort for ipn.HostPort creation. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Remove the returned error for AsServiceName Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * apply suggested code and comment Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace local dnsName in test with tailcfg.NoService Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: move noService back and use else where The constant serves the purpose of provide readability for passing as a function parameter. It's more meaningful comparing to a . It can just be an empty string in other places. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * ipn: Make WebHandlerExists and RemoveTCPForwarding accept svcName This commit replaces two functions' string input with svcName input since they only use the dnsName to identify service. Also did some minor cleanups Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> --------- Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
@@ -141,6 +141,8 @@ type localServeClient interface {
|
||||
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
|
||||
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error)
|
||||
IncrementCounter(ctx context.Context, name string, delta int) error
|
||||
GetPrefs(ctx context.Context) (*ipn.Prefs, error)
|
||||
EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// v2 specific flags
|
||||
bg bool // background mode
|
||||
setPath string // serve path
|
||||
https uint // HTTP port
|
||||
http uint // HTTP port
|
||||
tcp uint // TCP port
|
||||
tlsTerminatedTCP uint // a TLS terminated TCP port
|
||||
subcmd serveMode // subcommand
|
||||
yes bool // update without prompt
|
||||
bg bgBoolFlag // background mode
|
||||
setPath string // serve path
|
||||
https uint // HTTP port
|
||||
http uint // HTTP port
|
||||
tcp uint // TCP port
|
||||
tlsTerminatedTCP uint // a TLS terminated TCP port
|
||||
subcmd serveMode // subcommand
|
||||
yes bool // update without prompt
|
||||
service tailcfg.ServiceName // service name
|
||||
tun bool // redirect traffic to OS for service
|
||||
|
||||
lc localServeClient // localClient interface, specific to serve
|
||||
|
||||
@@ -354,7 +358,7 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
if sc.IsTCPForwardingOnPort(srvPort, noService) {
|
||||
fmt.Fprintf(Stderr, "error: cannot serve web; already serving TCP\n")
|
||||
return errHelp
|
||||
}
|
||||
@@ -411,11 +415,11 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mou
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
if sc.IsTCPForwardingOnPort(srvPort, noService) {
|
||||
return errors.New("cannot remove web handler; currently serving TCP")
|
||||
}
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
if !sc.WebHandlerExists(hp, mount) {
|
||||
if !sc.WebHandlerExists(noService, hp, mount) {
|
||||
return errors.New("error: handler does not exist")
|
||||
}
|
||||
sc.RemoveWebHandler(dnsName, srvPort, []string{mount}, false)
|
||||
@@ -550,15 +554,15 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
|
||||
|
||||
fwdAddr := "127.0.0.1:" + dstPortStr
|
||||
|
||||
if sc.IsServingWeb(srcPort) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||
}
|
||||
|
||||
dnsName, err := e.getSelfDNSName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sc.IsServingWeb(srcPort, noService) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||
}
|
||||
|
||||
sc.SetTCPForwarding(srcPort, fwdAddr, terminateTLS, dnsName)
|
||||
|
||||
if !reflect.DeepEqual(cursc, sc) {
|
||||
@@ -581,11 +585,11 @@ func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
if sc.IsServingWeb(src) {
|
||||
if sc.IsServingWeb(src, noService) {
|
||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
||||
}
|
||||
if ph := sc.GetTCPPortHandler(src); ph != nil {
|
||||
sc.RemoveTCPForwarding(src)
|
||||
if ph := sc.GetTCPPortHandler(src, noService); ph != nil {
|
||||
sc.RemoveTCPForwarding(noService, src)
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
return errors.New("error: serve config does not exist")
|
||||
@@ -682,7 +686,7 @@ func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) erro
|
||||
}
|
||||
|
||||
scheme := "https"
|
||||
if sc.IsServingHTTP(port) {
|
||||
if sc.IsServingHTTP(port, noService) {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
|
@@ -859,6 +859,7 @@ type fakeLocalServeClient struct {
|
||||
config *ipn.ServeConfig
|
||||
setCount int // counts calls to SetServeConfig
|
||||
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.
|
||||
@@ -891,6 +892,21 @@ func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.
|
||||
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()
|
||||
}
|
||||
lc.prefs.ApplyEdits(prefs)
|
||||
return lc.prefs, nil
|
||||
}
|
||||
|
||||
type mockQueryFeatureResponse struct {
|
||||
resp *tailcfg.QueryFeatureResponse
|
||||
err error
|
||||
|
@@ -18,6 +18,7 @@ import (
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -41,6 +42,55 @@ type commandInfo struct {
|
||||
LongHelp string
|
||||
}
|
||||
|
||||
type serviceNameFlag struct {
|
||||
Value *tailcfg.ServiceName
|
||||
}
|
||||
|
||||
func (s *serviceNameFlag) Set(sv string) error {
|
||||
if sv == "" {
|
||||
s.Value = new(tailcfg.ServiceName)
|
||||
return nil
|
||||
}
|
||||
v := tailcfg.ServiceName(sv)
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid service name: %q", sv)
|
||||
}
|
||||
*s.Value = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the string representation of service name.
|
||||
func (s *serviceNameFlag) String() string {
|
||||
return s.Value.String()
|
||||
}
|
||||
|
||||
type bgBoolFlag struct {
|
||||
Value bool
|
||||
IsSet bool // tracks if the flag was set by the user
|
||||
}
|
||||
|
||||
// Set sets the boolean flag and whether it's explicitly set by user based on the string value.
|
||||
func (b *bgBoolFlag) Set(s string) error {
|
||||
v, err := strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Value = v
|
||||
b.IsSet = 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 {
|
||||
if !b.IsSet {
|
||||
return "default"
|
||||
}
|
||||
return strconv.FormatBool(b.Value)
|
||||
}
|
||||
|
||||
var serveHelpCommon = strings.TrimSpace(`
|
||||
<target> can be a file, directory, text, or most commonly the location to a service running on the
|
||||
local machine. The location to the location service can be expressed as a port number (e.g., 3000),
|
||||
@@ -73,8 +123,11 @@ const (
|
||||
serveTypeHTTP
|
||||
serveTypeTCP
|
||||
serveTypeTLSTerminatedTCP
|
||||
serveTypeTUN
|
||||
)
|
||||
|
||||
const noService tailcfg.ServiceName = ""
|
||||
|
||||
var infoMap = map[serveMode]commandInfo{
|
||||
serve: {
|
||||
Name: "serve",
|
||||
@@ -120,7 +173,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
Exec: e.runServeCombined(subcmd),
|
||||
|
||||
FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.bg, "bg", false, "Run the command as a background process (default false)")
|
||||
fs.Var(&e.bg, "bg", "Run the command as a background process (default false, when --service is set defaults to true).")
|
||||
fs.StringVar(&e.setPath, "set-path", "", "Appends the specified path to the base URL for accessing the underlying service")
|
||||
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
|
||||
if subcmd == serve {
|
||||
@@ -128,7 +181,9 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
}
|
||||
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
||||
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
||||
fs.Var(&serviceNameFlag{Value: &e.service}, "service", "Serve for a service with distinct virtual IP instead on node itself.")
|
||||
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. Refer to docs for more information.")
|
||||
}),
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
Subcommands: []*ffcli.Command{
|
||||
@@ -162,9 +217,16 @@ func (e *serveEnv) validateArgs(subcmd serveMode, args []string) error {
|
||||
fmt.Fprint(e.stderr(), "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n")
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
if len(args) == 0 && e.tun {
|
||||
return nil
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
if e.tun && len(args) > 1 {
|
||||
fmt.Fprintln(e.stderr(), "Error: invalid argument format")
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
if len(args) > 2 {
|
||||
fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args))
|
||||
return errHelpFunc(subcmd)
|
||||
@@ -206,7 +268,16 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
forService := e.service != ""
|
||||
if !e.bg.IsSet {
|
||||
e.bg.Value = forService
|
||||
}
|
||||
|
||||
funnel := subcmd == funnel
|
||||
if forService && funnel {
|
||||
return errors.New("Error: --service flag is not supported with funnel")
|
||||
}
|
||||
|
||||
if funnel {
|
||||
// verify node has funnel capabilities
|
||||
if err := e.verifyFunnelEnabled(ctx, 443); err != nil {
|
||||
@@ -214,6 +285,10 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
}
|
||||
}
|
||||
|
||||
if forService && !e.bg.Value {
|
||||
return errors.New("Error: --service flag is only compatible with background mode")
|
||||
}
|
||||
|
||||
mount, err := cleanURLPath(e.setPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clean the mount point: %w", err)
|
||||
@@ -246,7 +321,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
// foreground or background.
|
||||
parentSC := sc
|
||||
|
||||
turnOff := "off" == args[len(args)-1]
|
||||
turnOff := len(args) > 0 && "off" == args[len(args)-1]
|
||||
if !turnOff && srvType == serveTypeHTTPS {
|
||||
// Running serve with https requires that the tailnet has enabled
|
||||
// https cert provisioning. Send users through an interactive flow
|
||||
@@ -263,10 +338,19 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
}
|
||||
|
||||
var watcher *tailscale.IPNBusWatcher
|
||||
wantFg := !e.bg && !turnOff
|
||||
svcName := noService
|
||||
|
||||
if forService {
|
||||
svcName = e.service
|
||||
dnsName = e.service.String()
|
||||
}
|
||||
if !forService && srvType == serveTypeTUN {
|
||||
return errors.New("tun mode is only supported for services")
|
||||
}
|
||||
wantFg := !e.bg.Value && !turnOff
|
||||
if wantFg {
|
||||
// validate the config before creating a WatchIPNBus session
|
||||
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
||||
if err := e.validateConfig(parentSC, srvPort, srvType, svcName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -292,12 +376,20 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
|
||||
var msg string
|
||||
if turnOff {
|
||||
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 {
|
||||
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
||||
if err := e.validateConfig(parentSC, srvPort, srvType, svcName); err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
|
||||
if forService {
|
||||
e.addServiceToPrefs(ctx, svcName.String())
|
||||
}
|
||||
target := ""
|
||||
if len(args) > 0 {
|
||||
target = args[0]
|
||||
}
|
||||
err = e.setServe(sc, dnsName, srvType, srvPort, mount, target, funnel)
|
||||
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -332,22 +424,66 @@ 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"
|
||||
|
||||
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType) error {
|
||||
sc, isFg := sc.FindConfig(port)
|
||||
if sc == nil {
|
||||
return nil
|
||||
// validateConfig checks if the serve config is valid to serve the type wanted on the port.
|
||||
// dnsName is a FQDN or a serviceName (with `svc:` prefix).
|
||||
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType, svcName tailcfg.ServiceName) error {
|
||||
var tcpHandlerForPort *ipn.TCPPortHandler
|
||||
if svcName != noService {
|
||||
svc := sc.Services[svcName]
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
if wantServe == serveTypeTUN && (svc.TCP != nil || svc.Web != nil) {
|
||||
return errors.New("service already has a TCP or Web handler, cannot serve in TUN mode")
|
||||
}
|
||||
if svc.Tun && wantServe != serveTypeTUN {
|
||||
return errors.New("service is already being served in TUN mode")
|
||||
}
|
||||
if svc.TCP[port] == nil {
|
||||
return nil
|
||||
}
|
||||
tcpHandlerForPort = svc.TCP[port]
|
||||
} else {
|
||||
sc, isFg := sc.FindConfig(port)
|
||||
if sc == nil {
|
||||
return nil
|
||||
}
|
||||
if isFg {
|
||||
return errors.New("foreground already exists under this port")
|
||||
}
|
||||
if !e.bg.Value {
|
||||
return fmt.Errorf(backgroundExistsMsg, infoMap[e.subcmd].Name, wantServe.String(), port)
|
||||
}
|
||||
tcpHandlerForPort = sc.TCP[port]
|
||||
}
|
||||
if isFg {
|
||||
return errors.New("foreground already exists under this port")
|
||||
}
|
||||
if !e.bg {
|
||||
return fmt.Errorf(backgroundExistsMsg, infoMap[e.subcmd].Name, wantServe.String(), port)
|
||||
}
|
||||
existingServe := serveFromPortHandler(sc.TCP[port])
|
||||
existingServe := serveFromPortHandler(tcpHandlerForPort)
|
||||
if wantServe != existingServe {
|
||||
return fmt.Errorf("want %q but port is already serving %q", wantServe, existingServe)
|
||||
target := svcName
|
||||
if target == noService {
|
||||
target = "machine"
|
||||
}
|
||||
return fmt.Errorf("want to serve %q but port is already serving %q for %q", wantServe, existingServe, target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -367,7 +503,7 @@ func serveFromPortHandler(tcp *ipn.TCPPortHandler) serveType {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool) error {
|
||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool) error {
|
||||
// update serve config based on the type
|
||||
switch srvType {
|
||||
case serveTypeHTTPS, serveTypeHTTP:
|
||||
@@ -380,45 +516,61 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
|
||||
if e.setPath != "" {
|
||||
return fmt.Errorf("cannot mount a path for TCP serve")
|
||||
}
|
||||
|
||||
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply TCP serve: %w", err)
|
||||
}
|
||||
case serveTypeTUN:
|
||||
// Caller checks that TUN mode is only supported for services.
|
||||
svcName := tailcfg.ServiceName(dnsName)
|
||||
if _, ok := sc.Services[svcName]; !ok {
|
||||
mak.Set(&sc.Services, svcName, new(ipn.ServiceConfig))
|
||||
}
|
||||
sc.Services[svcName].Tun = true
|
||||
default:
|
||||
return fmt.Errorf("invalid type %q", srvType)
|
||||
}
|
||||
|
||||
// update the serve config based on if funnel is enabled
|
||||
e.applyFunnel(sc, dnsName, srvPort, allowFunnel)
|
||||
|
||||
// Since funnel is not supported for services, we only apply it for node's serve.
|
||||
if svcName := tailcfg.AsServiceName(dnsName); svcName == noService {
|
||||
e.applyFunnel(sc, dnsName, srvPort, allowFunnel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
msgFunnelAvailable = "Available on the internet:"
|
||||
msgServeAvailable = "Available within your tailnet:"
|
||||
msgRunningInBackground = "%s started and running in the background."
|
||||
msgDisableProxy = "To disable the proxy, run: tailscale %s --%s=%d off"
|
||||
msgToExit = "Press Ctrl+C to exit."
|
||||
msgFunnelAvailable = "Available on the internet:"
|
||||
msgServeAvailable = "Available within your tailnet:"
|
||||
msgServiceWaitingApproval = "This machine is configured as a service proxy for %s, but approval from an admin is required. Once approved, it will be available in your Tailnet as:"
|
||||
msgRunningInBackground = "%s started and running in the background."
|
||||
msgRunningTunService = "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
|
||||
// serve config and status.
|
||||
func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16) string {
|
||||
var output strings.Builder
|
||||
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
|
||||
if sc.AllowFunnel[hp] == true {
|
||||
output.WriteString(msgFunnelAvailable)
|
||||
} else {
|
||||
output.WriteString(msgServeAvailable)
|
||||
svcName := tailcfg.AsServiceName(dnsName)
|
||||
forService := svcName != noService
|
||||
var webConfig *ipn.WebServerConfig
|
||||
var tcpHandler *ipn.TCPPortHandler
|
||||
ips := st.TailscaleIPs
|
||||
host := dnsName
|
||||
displayedHost := dnsName
|
||||
if forService {
|
||||
displayedHost = strings.Join([]string{svcName.WithoutPrefix(), st.CurrentTailnet.MagicDNSSuffix}, ".")
|
||||
host = svcName.WithoutPrefix()
|
||||
}
|
||||
output.WriteString("\n\n")
|
||||
hp := ipn.HostPort(net.JoinHostPort(host, strconv.Itoa(int(srvPort))))
|
||||
|
||||
scheme := "https"
|
||||
if sc.IsServingHTTP(srvPort) {
|
||||
if sc.IsServingHTTP(srvPort, svcName) {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
@@ -439,37 +591,68 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
if forService {
|
||||
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(fmt.Sprintf(msgServiceWaitingApproval, svcName.String()))
|
||||
ips = nil
|
||||
} else {
|
||||
output.WriteString(msgServeAvailable)
|
||||
ips = serviceIPMaps[0][svcName]
|
||||
}
|
||||
output.WriteString("\n\n")
|
||||
svc := sc.Services[svcName]
|
||||
if srvType == serveTypeTUN && svc.Tun {
|
||||
output.WriteString(fmt.Sprintf(msgRunningTunService, displayedHost))
|
||||
output.WriteString("\n")
|
||||
output.WriteString(fmt.Sprintf(msgDisableServiceTun, dnsName))
|
||||
output.WriteString("\n")
|
||||
output.WriteString(fmt.Sprintf(msgDisableService, dnsName))
|
||||
return output.String()
|
||||
}
|
||||
if svc != nil {
|
||||
webConfig = svc.Web[hp]
|
||||
tcpHandler = svc.TCP[srvPort]
|
||||
}
|
||||
} else {
|
||||
if sc.AllowFunnel[hp] == true {
|
||||
output.WriteString(msgFunnelAvailable)
|
||||
} else {
|
||||
output.WriteString(msgServeAvailable)
|
||||
}
|
||||
output.WriteString("\n\n")
|
||||
webConfig = sc.Web[hp]
|
||||
tcpHandler = sc.TCP[srvPort]
|
||||
}
|
||||
|
||||
if sc.Web[hp] != nil {
|
||||
mounts := slicesx.MapKeys(sc.Web[hp].Handlers)
|
||||
if webConfig != nil {
|
||||
mounts := slicesx.MapKeys(webConfig.Handlers)
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return len(mounts[i]) < len(mounts[j])
|
||||
})
|
||||
|
||||
for _, m := range mounts {
|
||||
h := sc.Web[hp].Handlers[m]
|
||||
t, d := srvTypeAndDesc(h)
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, dnsName, portPart, m))
|
||||
t, d := srvTypeAndDesc(webConfig.Handlers[m])
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, displayedHost, portPart, m))
|
||||
output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d))
|
||||
}
|
||||
} else if sc.TCP[srvPort] != nil {
|
||||
h := sc.TCP[srvPort]
|
||||
} else if tcpHandler != nil {
|
||||
|
||||
tlsStatus := "TLS over TCP"
|
||||
if h.TerminateTLS != "" {
|
||||
if tcpHandler.TerminateTLS != "" {
|
||||
tlsStatus = "TLS terminated"
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s\n", scheme, dnsName, portPart))
|
||||
output.WriteString(fmt.Sprintf("|-- tcp://%s (%s)\n", hp, tlsStatus))
|
||||
for _, a := range st.TailscaleIPs {
|
||||
output.WriteString(fmt.Sprintf("|-- tcp://%s:%d (%s)\n", displayedHost, srvPort, tlsStatus))
|
||||
for _, a := range ips {
|
||||
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort)))
|
||||
output.WriteString(fmt.Sprintf("|-- tcp://%s\n", ipp))
|
||||
}
|
||||
output.WriteString(fmt.Sprintf("|--> tcp://%s\n", h.TCPForward))
|
||||
output.WriteString(fmt.Sprintf("|--> tcp://%s\n\n", tcpHandler.TCPForward))
|
||||
}
|
||||
|
||||
if !e.bg {
|
||||
if !forService && !e.bg.Value {
|
||||
output.WriteString(msgToExit)
|
||||
return output.String()
|
||||
}
|
||||
@@ -479,14 +662,19 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
|
||||
output.WriteString(fmt.Sprintf(msgRunningInBackground, subCmdUpper))
|
||||
output.WriteString("\n")
|
||||
output.WriteString(fmt.Sprintf(msgDisableProxy, subCmd, srvType.String(), srvPort))
|
||||
if forService {
|
||||
output.WriteString(fmt.Sprintf(msgDisableServiceProxy, dnsName, srvType.String(), srvPort))
|
||||
output.WriteString("\n")
|
||||
output.WriteString(fmt.Sprintf(msgDisableService, dnsName))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf(msgDisableProxy, subCmd, srvType.String(), srvPort))
|
||||
}
|
||||
|
||||
return output.String()
|
||||
}
|
||||
|
||||
func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target string) error {
|
||||
h := new(ipn.HTTPHandler)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(target, "text:"):
|
||||
text := strings.TrimPrefix(target, "text:")
|
||||
@@ -522,7 +710,8 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
||||
}
|
||||
|
||||
// TODO: validation needs to check nested foreground configs
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
svcName := tailcfg.AsServiceName(dnsName)
|
||||
if sc.IsTCPForwardingOnPort(srvPort, svcName) {
|
||||
return errors.New("cannot serve web; already serving TCP")
|
||||
}
|
||||
|
||||
@@ -553,8 +742,9 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
|
||||
}
|
||||
|
||||
// TODO: needs to account for multiple configs from foreground mode
|
||||
if sc.IsServingWeb(srcPort) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||
svcName := tailcfg.AsServiceName(dnsName)
|
||||
if sc.IsServingWeb(srcPort, svcName) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d for %s", srcPort, dnsName)
|
||||
}
|
||||
|
||||
sc.SetTCPForwarding(srcPort, dstURL.Host, terminateTLS, dnsName)
|
||||
@@ -578,18 +768,24 @@ func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint
|
||||
}
|
||||
|
||||
// unsetServe removes the serve config for the given serve port.
|
||||
func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string) error {
|
||||
// dnsName is a FQDN or a serviceName (with `svc:` prefix).
|
||||
func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string) error {
|
||||
switch srvType {
|
||||
case serveTypeHTTPS, serveTypeHTTP:
|
||||
err := e.removeWebServe(sc, dnsName, srvPort, mount)
|
||||
err := e.removeWebServe(sc, st, dnsName, srvPort, mount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove web serve: %w", err)
|
||||
}
|
||||
case serveTypeTCP, serveTypeTLSTerminatedTCP:
|
||||
err := e.removeTCPServe(sc, srvPort)
|
||||
err := e.removeTCPServe(sc, dnsName, srvPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove TCP serve: %w", err)
|
||||
}
|
||||
case serveTypeTUN:
|
||||
err := e.removeTunServe(sc, dnsName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove TUN serve: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid type %q", srvType)
|
||||
}
|
||||
@@ -620,11 +816,16 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, er
|
||||
}
|
||||
}
|
||||
|
||||
if e.tun {
|
||||
srcTypeCount++
|
||||
srvType = serveTypeTUN
|
||||
}
|
||||
|
||||
if srcTypeCount > 1 {
|
||||
return 0, 0, fmt.Errorf("cannot serve multiple types for a single mount point")
|
||||
} else if srcTypeCount == 0 {
|
||||
srvType = serveTypeHTTPS
|
||||
srvPort = 443
|
||||
}
|
||||
if srcTypeCount == 0 {
|
||||
return serveTypeHTTPS, 443, nil
|
||||
}
|
||||
|
||||
return srvType, srvPort, nil
|
||||
@@ -728,32 +929,48 @@ func isLegacyInvocation(subcmd serveMode, args []string) (string, bool) {
|
||||
// and removes funnel if no remaining mounts exist for the serve port.
|
||||
// The srvPort argument is the serving port and the mount argument is
|
||||
// the mount point or registered path to remove.
|
||||
func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, mount string) error {
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
return errors.New("cannot remove web handler; currently serving TCP")
|
||||
func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16, mount string) error {
|
||||
if sc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
portStr := strconv.Itoa(int(srvPort))
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, portStr))
|
||||
hostName := dnsName
|
||||
webServeMap := sc.Web
|
||||
svcName := tailcfg.AsServiceName(dnsName)
|
||||
forService := svcName != noService
|
||||
if forService {
|
||||
svc := sc.Services[svcName]
|
||||
if svc == nil {
|
||||
return errors.New("service does not exist")
|
||||
}
|
||||
hostName = svcName.WithoutPrefix()
|
||||
webServeMap = svc.Web
|
||||
}
|
||||
|
||||
hp := ipn.HostPort(net.JoinHostPort(hostName, portStr))
|
||||
|
||||
if sc.IsTCPForwardingOnPort(srvPort, svcName) {
|
||||
return errors.New("cannot remove web handler; currently serving TCP")
|
||||
}
|
||||
var targetExists bool
|
||||
var mounts []string
|
||||
// mount is deduced from e.setPath but it is ambiguous as
|
||||
// to whether the user explicitly passed "/" or it was defaulted to.
|
||||
if e.setPath == "" {
|
||||
targetExists = sc.Web[hp] != nil && len(sc.Web[hp].Handlers) > 0
|
||||
targetExists = webServeMap[hp] != nil && len(webServeMap[hp].Handlers) > 0
|
||||
if targetExists {
|
||||
for mount := range sc.Web[hp].Handlers {
|
||||
for mount := range webServeMap[hp].Handlers {
|
||||
mounts = append(mounts, mount)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
targetExists = sc.WebHandlerExists(hp, mount)
|
||||
targetExists = sc.WebHandlerExists(svcName, hp, mount)
|
||||
mounts = []string{mount}
|
||||
}
|
||||
|
||||
if !targetExists {
|
||||
return errors.New("error: handler does not exist")
|
||||
return errors.New("handler does not exist")
|
||||
}
|
||||
|
||||
if len(mounts) > 1 {
|
||||
@@ -763,23 +980,47 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
|
||||
}
|
||||
}
|
||||
|
||||
sc.RemoveWebHandler(dnsName, srvPort, mounts, true)
|
||||
if forService {
|
||||
sc.RemoveServiceWebHandler(st, svcName, srvPort, mounts)
|
||||
} else {
|
||||
sc.RemoveWebHandler(dnsName, srvPort, mounts, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeTCPServe removes the TCP forwarding configuration for the
|
||||
// given srvPort, or serving port.
|
||||
func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error {
|
||||
// given srvPort, or serving port for the given dnsName.
|
||||
func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, dnsName string, src uint16) error {
|
||||
if sc == nil {
|
||||
return nil
|
||||
}
|
||||
if sc.GetTCPPortHandler(src) == nil {
|
||||
return errors.New("error: serve config does not exist")
|
||||
svcName := tailcfg.AsServiceName(dnsName)
|
||||
if sc.GetTCPPortHandler(src, svcName) == nil {
|
||||
return errors.New("serve config does not exist")
|
||||
}
|
||||
if sc.IsServingWeb(src) {
|
||||
if sc.IsServingWeb(src, svcName) {
|
||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
||||
}
|
||||
sc.RemoveTCPForwarding(src)
|
||||
sc.RemoveTCPForwarding(svcName, 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("service does not exist")
|
||||
}
|
||||
if !svc.Tun {
|
||||
return errors.New("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
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -262,7 +262,7 @@ func printFunnelStatus(ctx context.Context) {
|
||||
}
|
||||
sni, portStr, _ := net.SplitHostPort(string(hp))
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
isTCP := sc.IsTCPForwardingOnPort(uint16(p))
|
||||
isTCP := sc.IsTCPForwardingOnPort(uint16(p), noService)
|
||||
url := "https://"
|
||||
if isTCP {
|
||||
url = "tcp://"
|
||||
|
@@ -1007,8 +1007,6 @@ func allNumeric(s string) bool {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) webServerConfig(hostname string, forVIPService tailcfg.ServiceName, port uint16) (c ipn.WebServerConfigView, ok bool) {
|
||||
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
@@ -1016,8 +1014,10 @@ func (b *LocalBackend) webServerConfig(hostname string, forVIPService tailcfg.Se
|
||||
return c, false
|
||||
}
|
||||
if forVIPService != "" {
|
||||
key := ipn.HostPort(net.JoinHostPort(forVIPService.WithoutPrefix(), fmt.Sprintf("%d", port)))
|
||||
return b.serveConfig.FindServiceWeb(forVIPService, key)
|
||||
}
|
||||
key := ipn.HostPort(net.JoinHostPort(hostname, fmt.Sprintf("%d", port)))
|
||||
return b.serveConfig.FindWeb(key)
|
||||
}
|
||||
|
||||
|
214
ipn/serve.go
214
ipn/serve.go
@@ -166,26 +166,44 @@ type HTTPHandler struct {
|
||||
|
||||
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
|
||||
// the given host:port and mount point.
|
||||
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
|
||||
h := sc.GetWebHandler(hp, mount)
|
||||
func (sc *ServeConfig) WebHandlerExists(svcName tailcfg.ServiceName, hp HostPort, mount string) bool {
|
||||
h := sc.GetWebHandler(svcName, hp, mount)
|
||||
return h != nil
|
||||
}
|
||||
|
||||
// GetWebHandler returns the HTTPHandler for the given host:port and mount point.
|
||||
// Returns nil if the handler does not exist.
|
||||
func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler {
|
||||
if sc == nil || sc.Web[hp] == nil {
|
||||
func (sc *ServeConfig) GetWebHandler(svcName tailcfg.ServiceName, hp HostPort, mount string) *HTTPHandler {
|
||||
if sc == nil {
|
||||
return nil
|
||||
}
|
||||
if svcName != "" {
|
||||
if svc, ok := sc.Services[svcName]; ok && svc.Web != nil {
|
||||
if webCfg, ok := svc.Web[hp]; ok {
|
||||
return webCfg.Handlers[mount]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if sc.Web[hp] == nil {
|
||||
return nil
|
||||
}
|
||||
return sc.Web[hp].Handlers[mount]
|
||||
}
|
||||
|
||||
// GetTCPPortHandler returns the TCPPortHandler for the given port.
|
||||
// If the port is not configured, nil is returned.
|
||||
func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
|
||||
// GetTCPPortHandler returns the TCPPortHandler for the given port. If the port
|
||||
// is not configured, nil is returned. Parameter svcName can be tailcfg.NoService
|
||||
// for local serve or a service name for a service hosted on node.
|
||||
func (sc *ServeConfig) GetTCPPortHandler(port uint16, svcName tailcfg.ServiceName) *TCPPortHandler {
|
||||
if sc == nil {
|
||||
return nil
|
||||
}
|
||||
if svcName != "" {
|
||||
if svc, ok := sc.Services[svcName]; ok && svc != nil {
|
||||
return svc.TCP[port]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return sc.TCP[port]
|
||||
}
|
||||
|
||||
@@ -227,34 +245,78 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding
|
||||
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving.
|
||||
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
|
||||
if sc == nil || sc.TCP[port] == nil {
|
||||
// IsTCPForwardingOnPort reports whether ServeConfig is currently forwarding
|
||||
// in TCPForward mode on the given port for local or a service. svcName will
|
||||
// either be noService (empty string) for local serve or a serviceName for service
|
||||
// hosted on node. Notice TCPForwarding is exclusive with Web/HTTPS serving.
|
||||
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16, svcName tailcfg.ServiceName) bool {
|
||||
if sc == nil {
|
||||
return false
|
||||
}
|
||||
return !sc.IsServingWeb(port)
|
||||
}
|
||||
|
||||
// IsServingWeb reports whether if ServeConfig is currently serving Web
|
||||
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingWeb(port uint16) bool {
|
||||
return sc.IsServingHTTP(port) || sc.IsServingHTTPS(port)
|
||||
}
|
||||
|
||||
// IsServingHTTPS reports whether if ServeConfig is currently serving HTTPS on
|
||||
// the given port. This is exclusive of HTTP and TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingHTTPS(port uint16) bool {
|
||||
if sc == nil || sc.TCP[port] == nil {
|
||||
if svcName != "" {
|
||||
svc, ok := sc.Services[svcName]
|
||||
if !ok || svc == nil {
|
||||
return false
|
||||
}
|
||||
if svc.TCP[port] == nil {
|
||||
return false
|
||||
}
|
||||
} else if sc.TCP[port] == nil {
|
||||
return false
|
||||
}
|
||||
return sc.TCP[port].HTTPS
|
||||
return !sc.IsServingWeb(port, svcName)
|
||||
}
|
||||
|
||||
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the
|
||||
// given port. This is exclusive of HTTPS and TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingHTTP(port uint16) bool {
|
||||
if sc == nil || sc.TCP[port] == nil {
|
||||
// IsServingWeb reports whether ServeConfig is currently serving Web (HTTP/HTTPS)
|
||||
// on the given port for local or a service. svcName will be either tailcfg.NoService,
|
||||
// or a serviceName for service hosted on node. This is exclusive with TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingWeb(port uint16, svcName tailcfg.ServiceName) bool {
|
||||
return sc.IsServingHTTP(port, svcName) || sc.IsServingHTTPS(port, svcName)
|
||||
}
|
||||
|
||||
// IsServingHTTPS reports whether ServeConfig is currently serving HTTPS on
|
||||
// the given port for local or a service. svcName will be either tailcfg.NoService
|
||||
// for local serve, or a serviceName for service hosted on node. This is exclusive
|
||||
// with HTTP and TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingHTTPS(port uint16, svcName tailcfg.ServiceName) bool {
|
||||
if sc == nil {
|
||||
return false
|
||||
}
|
||||
var tcpHandlers map[uint16]*TCPPortHandler
|
||||
if svcName != "" {
|
||||
if svc := sc.Services[svcName]; svc != nil {
|
||||
tcpHandlers = svc.TCP
|
||||
}
|
||||
} else {
|
||||
tcpHandlers = sc.TCP
|
||||
}
|
||||
|
||||
th := tcpHandlers[port]
|
||||
if th == nil {
|
||||
return false
|
||||
}
|
||||
return th.HTTPS
|
||||
}
|
||||
|
||||
// IsServingHTTP reports whether ServeConfig is currently serving HTTP on the
|
||||
// given port for local or a service. svcName will be either tailcfg.NoService for
|
||||
// local serve, or a serviceName for service hosted on node. This is exclusive
|
||||
// with HTTPS and TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingHTTP(port uint16, svcName tailcfg.ServiceName) bool {
|
||||
if sc == nil {
|
||||
return false
|
||||
}
|
||||
if svcName != "" {
|
||||
if svc := sc.Services[svcName]; svc != nil {
|
||||
if svc.TCP[port] != nil {
|
||||
return svc.TCP[port].HTTP
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.TCP[port] == nil {
|
||||
return false
|
||||
}
|
||||
return sc.TCP[port].HTTP
|
||||
@@ -280,21 +342,37 @@ func (sc *ServeConfig) FindConfig(port uint16) (*ServeConfig, bool) {
|
||||
|
||||
// SetWebHandler sets the given HTTPHandler at the specified host, port,
|
||||
// and mount in the serve config. sc.TCP is also updated to reflect web
|
||||
// serving usage of the given port.
|
||||
// serving usage of the given port. The st argument is needed when setting
|
||||
// a web handler for a service, otherwise it can be nil.
|
||||
func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uint16, mount string, useTLS bool) {
|
||||
if sc == nil {
|
||||
sc = new(ServeConfig)
|
||||
}
|
||||
mak.Set(&sc.TCP, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
|
||||
|
||||
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
|
||||
if _, ok := sc.Web[hp]; !ok {
|
||||
mak.Set(&sc.Web, hp, new(WebServerConfig))
|
||||
tcpMap := &sc.TCP
|
||||
webServerMap := &sc.Web
|
||||
hostName := host
|
||||
if svcName := tailcfg.AsServiceName(host); svcName != "" {
|
||||
hostName = svcName.WithoutPrefix()
|
||||
svc, ok := sc.Services[svcName]
|
||||
if !ok {
|
||||
svc = new(ServiceConfig)
|
||||
mak.Set(&sc.Services, svcName, svc)
|
||||
}
|
||||
tcpMap = &svc.TCP
|
||||
webServerMap = &svc.Web
|
||||
}
|
||||
mak.Set(&sc.Web[hp].Handlers, mount, handler)
|
||||
|
||||
mak.Set(tcpMap, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
|
||||
hp := HostPort(net.JoinHostPort(hostName, strconv.Itoa(int(port))))
|
||||
webCfg, ok := (*webServerMap)[hp]
|
||||
if !ok {
|
||||
webCfg = new(WebServerConfig)
|
||||
mak.Set(webServerMap, hp, webCfg)
|
||||
}
|
||||
mak.Set(&webCfg.Handlers, mount, handler)
|
||||
// TODO(tylersmalley): handle multiple web handlers from foreground mode
|
||||
for k, v := range sc.Web[hp].Handlers {
|
||||
for k, v := range webCfg.Handlers {
|
||||
if v == handler {
|
||||
continue
|
||||
}
|
||||
@@ -305,7 +383,7 @@ func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uin
|
||||
m1 := strings.TrimSuffix(mount, "/")
|
||||
m2 := strings.TrimSuffix(k, "/")
|
||||
if m1 == m2 {
|
||||
delete(sc.Web[hp].Handlers, k)
|
||||
delete(webCfg.Handlers, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,9 +396,19 @@ func (sc *ServeConfig) SetTCPForwarding(port uint16, fwdAddr string, terminateTL
|
||||
if sc == nil {
|
||||
sc = new(ServeConfig)
|
||||
}
|
||||
mak.Set(&sc.TCP, port, &TCPPortHandler{TCPForward: fwdAddr})
|
||||
tcpPortHandler := &sc.TCP
|
||||
if svcName := tailcfg.AsServiceName(host); svcName != "" {
|
||||
svcConfig, ok := sc.Services[svcName]
|
||||
if !ok {
|
||||
svcConfig = new(ServiceConfig)
|
||||
mak.Set(&sc.Services, svcName, svcConfig)
|
||||
}
|
||||
tcpPortHandler = &svcConfig.TCP
|
||||
}
|
||||
mak.Set(tcpPortHandler, port, &TCPPortHandler{TCPForward: fwdAddr})
|
||||
|
||||
if terminateTLS {
|
||||
sc.TCP[port].TerminateTLS = host
|
||||
(*tcpPortHandler)[port].TerminateTLS = host
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,9 +432,9 @@ func (sc *ServeConfig) SetFunnel(host string, port uint16, setOn bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveWebHandler deletes the web handlers at all of the given mount points
|
||||
// for the provided host and port in the serve config. If cleanupFunnel is
|
||||
// true, this also removes the funnel value for this port if no handlers remain.
|
||||
// RemoveWebHandler deletes the web handlers at all of the given mount points for the
|
||||
// provided host and port in the serve config for the node (as opposed to a service).
|
||||
// If cleanupFunnel is true, this also removes the funnel value for this port if no handlers remain.
|
||||
func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []string, cleanupFunnel bool) {
|
||||
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
|
||||
|
||||
@@ -374,9 +462,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) {
|
||||
hostName := svcName.WithoutPrefix()
|
||||
hp := HostPort(net.JoinHostPort(hostName, 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
|
||||
// port from the serve config.
|
||||
func (sc *ServeConfig) RemoveTCPForwarding(port uint16) {
|
||||
func (sc *ServeConfig) RemoveTCPForwarding(svcName tailcfg.ServiceName, port uint16) {
|
||||
if svcName != "" {
|
||||
if svc := sc.Services[svcName]; 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, svcName)
|
||||
}
|
||||
if len(sc.Services) == 0 {
|
||||
sc.Services = nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
delete(sc.TCP, port)
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
|
@@ -128,6 +128,121 @@ func TestHasPathHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTCPForwardingOnPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg ServeConfig
|
||||
svcName tailcfg.ServiceName
|
||||
port uint16
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty-config",
|
||||
cfg: ServeConfig{},
|
||||
svcName: "",
|
||||
port: 80,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "node-tcp-config-match",
|
||||
cfg: ServeConfig{
|
||||
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
||||
},
|
||||
svcName: "",
|
||||
port: 80,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "node-tcp-config-no-match",
|
||||
cfg: ServeConfig{
|
||||
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
||||
},
|
||||
svcName: "",
|
||||
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"}},
|
||||
},
|
||||
svcName: "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!"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
svcName: "",
|
||||
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"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
svcName: "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"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
svcName: "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!"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
svcName: "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.svcName)
|
||||
if tt.want != got {
|
||||
t.Errorf("IsTCPForwardingOnPort() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandProxyTargetDev(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@@ -927,6 +927,16 @@ func (t *TPMInfo) Present() bool { return t != nil }
|
||||
// This is not related to the older [Service] used in [Hostinfo.Services].
|
||||
type ServiceName string
|
||||
|
||||
// AsServiceName reports whether the given string is a valid service name.
|
||||
// If so returns the name as a [tailcfg.ServiceName], otherwise returns "".
|
||||
func AsServiceName(s string) ServiceName {
|
||||
svcName := ServiceName(s)
|
||||
if err := svcName.Validate(); err != nil {
|
||||
return ""
|
||||
}
|
||||
return svcName
|
||||
}
|
||||
|
||||
// Validate validates if the service name is formatted correctly.
|
||||
// We only allow valid DNS labels, since the expectation is that these will be
|
||||
// used as parts of domain names. All errors are [vizerror.Error].
|
||||
|
Reference in New Issue
Block a user