summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorShayne Sweeney <shayne@tailscale.com>2023-04-10 22:57:51 -0400
committerShayne Sweeney <shayne@tailscale.com>2023-04-10 23:17:19 -0400
commit99b821c04cc43b31036f085dba1e5a9e2bae782e (patch)
tree381423d34c941ad9a120e22440f5806d783bf7fb
parentcef0a474f8fb1a09e4ce3191ff000fcc21858284 (diff)
downloadtailscale-shayne/funnel_cmd.tar.xz
tailscale-shayne/funnel_cmd.zip
cmd/tailscale/cli: [funnel] add https:<port> ... abilityshayne/funnel_cmd
Adds all-in-one ability to start a serve + Funnel with one command. Fixes #7844 Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
-rw-r--r--cmd/tailscale/cli/funnel.go74
-rw-r--r--cmd/tailscale/cli/serve_test.go60
2 files changed, 126 insertions, 8 deletions
diff --git a/cmd/tailscale/cli/funnel.go b/cmd/tailscale/cli/funnel.go
index 8e3e80004..74b337455 100644
--- a/cmd/tailscale/cli/funnel.go
+++ b/cmd/tailscale/cli/funnel.go
@@ -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 @@ funnel <serve-port> {on|off}
}
// 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
}
diff --git a/cmd/tailscale/cli/serve_test.go b/cmd/tailscale/cli/serve_test.go
index 8031b2b02..e111e3a1a 100644
--- a/cmd/tailscale/cli/serve_test.go
+++ b/cmd/tailscale/cli/serve_test.go
@@ -543,6 +543,61 @@ func TestServeConfigMutations(t *testing.T) {
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 @@ func TestServeConfigMutations(t *testing.T) {
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)