cmd/tailscale/cli: [funnel] add https:<port> ... ability

Adds all-in-one ability to start a serve + Funnel with one command.

Fixes #7844

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
This commit is contained in:
Shayne Sweeney 2023-04-10 22:57:51 -04:00
parent cef0a474f8
commit 99b821c04c
No known key found for this signature in database
GPG Key ID: 69DA13E86BF403B0
2 changed files with 126 additions and 8 deletions

View File

@ -33,14 +33,32 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
ShortUsage: strings.TrimSpace(`
funnel <serve-port> {on|off}
funnel status [--json]
funnel https:<port> <mount-point> <source> [off]
`),
LongHelp: strings.TrimSpace(`
*** BETA; all of this is subject to change ***
Funnel allows you to publish a Tailscale Serve
server publicly, open to the entire internet.
EXAMPLES
- To toggle Funnel on HTTPS port 443 (default):
$ tailscale funnel 443 on
$ tailscale funnel 443 off
Turning off Funnel only turns off serving to the internet.
It does not affect serving to your tailnet.
- To proxy requests to a web server at 127.0.0.1:3000:
$ tailscale funnel https:443 / http://127.0.0.1:3000
Or, using the default port:
$ tailscale funnel https / http://127.0.0.1:3000
- To serve a single file or a directory of files:
$ tailscale funnel https / /home/alice/blog/index.html
$ tailscale funnel https /images/ /home/alice/blog/images
`),
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",
"",
"Turning off Funnel only turns off serving to the internet.",
"It does not affect serving to your tailnet.",
}, "\n"),
Exec: e.runFunnel,
UsageFunc: usageFunc,
Subcommands: []*ffcli.Command{
@ -58,10 +76,50 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
}
// runFunnel is the entry point for the "tailscale funnel" subcommand and
// manages turning on/off funnel. Funnel is off by default.
// handles the following cases:
//
// 1. `tailscale funnel status`
// - Prints the current status of the Funnel service.
//
// 2. `tailscale funnel <serve-port> {on|off}`
// - Turns the Funnel service on or off.
//
// 3. `tailsclae funnel https(:<serve-port>) <mount-point> <source>`
// - Starts a serve command and turns the Funnel service on.
//
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
if len(args) == 2 {
switch args[1] {
case "on", "off":
return e.doToggleFunnel(ctx, args)
default:
return flag.ErrHelp
}
}
if len(args) > 2 {
if err := serveCmd.Exec(ctx, args); err != nil {
return err
}
_, portStr, _ := strings.Cut(args[0], ":")
if portStr == "" {
portStr = "443"
}
onOrOff := args[len(args)-1]
if onOrOff != "off" {
onOrOff = "on"
}
return e.doToggleFunnel(ctx, []string{portStr, onOrOff})
}
return flag.ErrHelp
}
// doToggleFunnel is the handler for "funnel <serve-port> {on|off}". It sets the
// Funnel service to on or off for the given port.
func (e *serveEnv) doToggleFunnel(ctx context.Context, args []string) error {
if len(args) != 2 {
return flag.ErrHelp
}

View File

@ -543,6 +543,61 @@ type step struct {
want: &ipn.ServeConfig{},
})
// Funnel HTTP tests
add(step{reset: true})
add(step{
command: cmd("funnel https / http://127.0.0.1:3000"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("funnel https / http://127.0.0.1:3000 off"),
want: &ipn.ServeConfig{},
})
add(step{
command: cmd("funnel https:443 / http://127.0.0.1:3000"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("funnel https:443 / http://127.0.0.1:3000 off"),
want: &ipn.ServeConfig{},
})
add(step{
command: cmd("funnel https:8443 / http://127.0.0.1:3000"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("funnel https:8443 / http://127.0.0.1:3000 off"),
want: &ipn.ServeConfig{},
})
add(step{ // invalid port
command: cmd("funnel https:8080 / http://127.0.0.1:3000"),
wantErr: anyErr(),
})
// tricky steps
add(step{reset: true})
add(step{ // a directory with a trailing slash mount point
@ -652,9 +707,14 @@ type step struct {
var args []string
if st.command[0] == "funnel" {
cmd = newFunnelCommand(e)
// The funnel command can call serve command, so we need to set the
// serveCmd variable.
serveCmd = newServeCommand(e)
args = st.command[1:]
} else {
cmd = newServeCommand(e)
// Reset the serveCmd variable from possible funnel command run.
serveCmd = cmd
args = st.command
}
err := cmd.ParseAndRun(context.Background(), args)