cmd/tailscale/cli: [serve] rework commands based on feedback (#6521)
``` $ tailscale serve https:<port> <mount-point> <source> [off] $ tailscale serve tcp:<port> tcp://localhost:<local-port> [off] $ tailscale serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off] $ tailscale serve status [--json] $ tailscale funnel <serve-port> {on|off} $ tailscale funnel status [--json] ``` Fixes: #6674 Signed-off-by: Shayne Sweeney <shayne@tailscale.com>pull/7701/head
parent
6ef2105a8e
commit
2b892ad6e7
|
@ -147,6 +147,8 @@ change in the future.
|
||||||
switch {
|
switch {
|
||||||
case slices.Contains(args, "debug"):
|
case slices.Contains(args, "debug"):
|
||||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||||
|
case slices.Contains(args, "funnel"):
|
||||||
|
rootCmd.Subcommands = append(rootCmd.Subcommands, funnelCmd)
|
||||||
case slices.Contains(args, "serve"):
|
case slices.Contains(args, "serve"):
|
||||||
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
|
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
|
||||||
case slices.Contains(args, "update"):
|
case slices.Contains(args, "update"):
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/util/mak"
|
||||||
|
)
|
||||||
|
|
||||||
|
var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
|
||||||
|
|
||||||
|
// newFunnelCommand returns a new "funnel" subcommand using e as its environment.
|
||||||
|
// The funnel subcommand is used to turn on/off the Funnel service.
|
||||||
|
// Funnel is off by default.
|
||||||
|
// Funnel allows you to publish a 'tailscale serve' server publicly, open to the
|
||||||
|
// entire internet.
|
||||||
|
// newFunnelCommand shares the same serveEnv as the "serve" subcommand. See
|
||||||
|
// newServeCommand and serve.go for more details.
|
||||||
|
func newFunnelCommand(e *serveEnv) *ffcli.Command {
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "funnel",
|
||||||
|
ShortHelp: "[ALPHA] turn Tailscale Funnel on or off",
|
||||||
|
ShortUsage: strings.TrimSpace(`
|
||||||
|
funnel <serve-port> {on|off}
|
||||||
|
funnel status [--json]
|
||||||
|
`),
|
||||||
|
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{
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Exec: e.runServeStatus,
|
||||||
|
ShortHelp: "show current serve/funnel status",
|
||||||
|
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
|
||||||
|
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||||
|
}),
|
||||||
|
UsageFunc: usageFunc,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runFunnel is the entry point for the "tailscale funnel" subcommand and
|
||||||
|
// manages turning on/off funnel. Funnel is off by default.
|
||||||
|
//
|
||||||
|
// 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 {
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
var on bool
|
||||||
|
switch args[1] {
|
||||||
|
case "on", "off":
|
||||||
|
on = args[1] == "on"
|
||||||
|
default:
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
sc, err := e.lc.GetServeConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sc == nil {
|
||||||
|
sc = new(ipn.ServeConfig)
|
||||||
|
}
|
||||||
|
st, err := e.getLocalClientStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting client status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
port64, err := strconv.ParseUint(args[0], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
port := uint16(port64)
|
||||||
|
|
||||||
|
if err := ipn.CheckFunnelAccess(port, st.Self.Capabilities); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||||
|
hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port)))
|
||||||
|
if on == sc.AllowFunnel[hp] {
|
||||||
|
printFunnelWarning(sc)
|
||||||
|
// Nothing to do.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if on {
|
||||||
|
mak.Set(&sc.AllowFunnel, hp, true)
|
||||||
|
} else {
|
||||||
|
delete(sc.AllowFunnel, hp)
|
||||||
|
// clear map mostly for testing
|
||||||
|
if len(sc.AllowFunnel) == 0 {
|
||||||
|
sc.AllowFunnel = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printFunnelWarning(sc)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// printFunnelWarning prints a warning if the Funnel is on but there is no serve
|
||||||
|
// config for its host:port.
|
||||||
|
func printFunnelWarning(sc *ipn.ServeConfig) {
|
||||||
|
var warn bool
|
||||||
|
for hp, a := range sc.AllowFunnel {
|
||||||
|
if !a {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, portStr, _ := net.SplitHostPort(string(hp))
|
||||||
|
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||||
|
if _, ok := sc.TCP[uint16(p)]; !ok {
|
||||||
|
warn = true
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: funnel=on for %s, but no serve config\n", hp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if warn {
|
||||||
|
fmt.Fprintf(os.Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,8 +35,11 @@ func newServeCommand(e *serveEnv) *ffcli.Command {
|
||||||
Name: "serve",
|
Name: "serve",
|
||||||
ShortHelp: "[ALPHA] Serve from your Tailscale node",
|
ShortHelp: "[ALPHA] Serve from your Tailscale node",
|
||||||
ShortUsage: strings.TrimSpace(`
|
ShortUsage: strings.TrimSpace(`
|
||||||
serve [flags] <mount-point> {proxy|path|text} <arg>
|
serve https:<port> <mount-point> <source> [off]
|
||||||
serve [flags] <sub-command> [sub-flags] <args>`),
|
serve tcp:<port> tcp://localhost:<local-port> [off]
|
||||||
|
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
|
||||||
|
serve status [--json]
|
||||||
|
`),
|
||||||
LongHelp: strings.TrimSpace(`
|
LongHelp: strings.TrimSpace(`
|
||||||
*** ALPHA; all of this is subject to change ***
|
*** ALPHA; all of this is subject to change ***
|
||||||
|
|
||||||
|
@ -45,68 +48,42 @@ content and local servers from your Tailscale node to
|
||||||
your tailnet.
|
your tailnet.
|
||||||
|
|
||||||
You can also choose to enable the Tailscale Funnel with:
|
You can also choose to enable the Tailscale Funnel with:
|
||||||
'tailscale serve funnel on'. Funnel allows you to publish
|
'tailscale funnel on'. Funnel allows you to publish
|
||||||
a 'tailscale serve' server publicly, open to the entire
|
a 'tailscale serve' server publicly, open to the entire
|
||||||
internet. See https://tailscale.com/funnel.
|
internet. See https://tailscale.com/funnel.
|
||||||
|
|
||||||
EXAMPLES
|
EXAMPLES
|
||||||
- To proxy requests to a web server at 127.0.0.1:3000:
|
- To proxy requests to a web server at 127.0.0.1:3000:
|
||||||
$ tailscale serve / proxy 3000
|
$ tailscale serve https:443 / http://127.0.0.1:3000
|
||||||
|
|
||||||
|
Or, using the default port:
|
||||||
|
$ tailscale serve https / http://127.0.0.1:3000
|
||||||
|
|
||||||
- To serve a single file or a directory of files:
|
- To serve a single file or a directory of files:
|
||||||
$ tailscale serve / path /home/alice/blog/index.html
|
$ tailscale serve https / /home/alice/blog/index.html
|
||||||
$ tailscale serve /images/ path /home/alice/blog/images
|
$ tailscale serve https /images/ /home/alice/blog/images
|
||||||
|
|
||||||
- To serve simple static text:
|
- To serve simple static text:
|
||||||
$ tailscale serve / text "Hello, world!"
|
$ tailscale serve https:8080 / text:"Hello, world!"
|
||||||
|
|
||||||
|
- To forward raw TCP packets to a local TCP server on port 5432:
|
||||||
|
$ tailscale serve tcp:2222 tcp://localhost:22
|
||||||
|
|
||||||
|
- To forward raw, TLS-terminated TCP packets to a local TCP server on port 80:
|
||||||
|
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
|
||||||
`),
|
`),
|
||||||
Exec: e.runServe,
|
Exec: e.runServe,
|
||||||
FlagSet: e.newFlags("serve", func(fs *flag.FlagSet) {
|
|
||||||
fs.BoolVar(&e.remove, "remove", false, "remove an existing serve config")
|
|
||||||
fs.UintVar(&e.servePort, "serve-port", 443, "port to serve on (443, 8443 or 10000)")
|
|
||||||
}),
|
|
||||||
UsageFunc: usageFunc,
|
UsageFunc: usageFunc,
|
||||||
Subcommands: []*ffcli.Command{
|
Subcommands: []*ffcli.Command{
|
||||||
{
|
{
|
||||||
Name: "status",
|
Name: "status",
|
||||||
Exec: e.runServeStatus,
|
Exec: e.runServeStatus,
|
||||||
ShortHelp: "show current serve status",
|
ShortHelp: "show current serve/funnel status",
|
||||||
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
|
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
|
||||||
fs.BoolVar(&e.json, "json", false, "output JSON")
|
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||||
}),
|
}),
|
||||||
UsageFunc: usageFunc,
|
UsageFunc: usageFunc,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "tcp",
|
|
||||||
Exec: e.runServeTCP,
|
|
||||||
ShortHelp: "add or remove a TCP port forward",
|
|
||||||
LongHelp: strings.Join([]string{
|
|
||||||
"EXAMPLES",
|
|
||||||
" - Forward TLS over TCP to a local TCP server on port 5432:",
|
|
||||||
" $ tailscale serve tcp 5432",
|
|
||||||
"",
|
|
||||||
" - Forward raw, TLS-terminated TCP packets to a local TCP server on port 5432:",
|
|
||||||
" $ tailscale serve tcp --terminate-tls 5432",
|
|
||||||
}, "\n"),
|
|
||||||
FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
|
|
||||||
fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
|
|
||||||
}),
|
|
||||||
UsageFunc: usageFunc,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "funnel",
|
|
||||||
Exec: e.runServeFunnel,
|
|
||||||
ShortUsage: "funnel [flags] {on|off}",
|
|
||||||
ShortHelp: "turn Tailscale Funnel on or off",
|
|
||||||
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"),
|
|
||||||
UsageFunc: usageFunc,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,9 +120,6 @@ type localServeClient interface {
|
||||||
// It also contains the flags, as registered with newServeCommand.
|
// It also contains the flags, as registered with newServeCommand.
|
||||||
type serveEnv struct {
|
type serveEnv struct {
|
||||||
// flags
|
// flags
|
||||||
servePort uint // Port to serve on. Defaults to 443.
|
|
||||||
terminateTLS bool
|
|
||||||
remove bool // remove a serve config
|
|
||||||
json bool // output JSON (status only for now)
|
json bool // output JSON (status only for now)
|
||||||
|
|
||||||
lc localServeClient // localClient interface, specific to serve
|
lc localServeClient // localClient interface, specific to serve
|
||||||
|
@ -186,24 +160,15 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateServePort returns --serve-port flag value,
|
|
||||||
// or an error if the port is not a valid port to serve on.
|
|
||||||
func (e *serveEnv) validateServePort() (port uint16, err error) {
|
|
||||||
// Make sure e.servePort is uint16.
|
|
||||||
port = uint16(e.servePort)
|
|
||||||
if uint(port) != e.servePort {
|
|
||||||
return 0, fmt.Errorf("serve-port %d is out of range", e.servePort)
|
|
||||||
}
|
|
||||||
return port, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// runServe is the entry point for the "serve" subcommand, managing Web
|
// runServe is the entry point for the "serve" subcommand, managing Web
|
||||||
// serve config types like proxy, path, and text.
|
// serve config types like proxy, path, and text.
|
||||||
//
|
//
|
||||||
// Examples:
|
// Examples:
|
||||||
// - tailscale serve / proxy 3000
|
// - tailscale serve https / http://localhost:3000
|
||||||
// - tailscale serve /images/ path /var/www/images/
|
// - tailscale serve https /images/ /var/www/images/
|
||||||
// - tailscale --serve-port=10000 serve /motd.txt text "Hello, world!"
|
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
|
||||||
|
// - tailscale serve tcp:2222 tcp://localhost:22
|
||||||
|
// - tailscale serve tls-terminated-tcp:443 tcp://localhost:80
|
||||||
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return flag.ErrHelp
|
return flag.ErrHelp
|
||||||
|
@ -223,39 +188,94 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||||
return e.lc.SetServeConfig(ctx, sc)
|
return e.lc.SetServeConfig(ctx, sc)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(len(args) == 3 || (e.remove && len(args) >= 1)) {
|
parsePort := func(portStr string) (uint16, error) {
|
||||||
|
port64, err := strconv.ParseUint(portStr, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uint16(port64), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
srcType, srcPortStr, found := strings.Cut(args[0], ":")
|
||||||
|
if !found {
|
||||||
|
if srcType == "https" && srcPortStr == "" {
|
||||||
|
// Default https port to 443.
|
||||||
|
srcPortStr = "443"
|
||||||
|
} else {
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
turnOff := "off" == args[len(args)-1]
|
||||||
|
|
||||||
|
if len(args) < 2 || (srcType == "https" && !turnOff && len(args) < 3) {
|
||||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
||||||
return flag.ErrHelp
|
return flag.ErrHelp
|
||||||
}
|
}
|
||||||
|
|
||||||
srvPort, err := e.validateServePort()
|
srcPort, err := parsePort(srcPortStr)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
srvPortStr := strconv.Itoa(int(srvPort))
|
|
||||||
|
|
||||||
mount, err := cleanMountPoint(args[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.remove {
|
switch srcType {
|
||||||
return e.handleWebServeRemove(ctx, mount)
|
case "https":
|
||||||
|
mount, err := cleanMountPoint(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if turnOff {
|
||||||
|
return e.handleWebServeRemove(ctx, srcPort, mount)
|
||||||
|
}
|
||||||
|
return e.handleWebServe(ctx, srcPort, mount, args[2])
|
||||||
|
case "tcp", "tls-terminated-tcp":
|
||||||
|
if turnOff {
|
||||||
|
return e.handleTCPServeRemove(ctx, srcPort)
|
||||||
|
}
|
||||||
|
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
|
||||||
|
fmt.Fprint(os.Stderr, "must be one of: https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleWebServe handles the "tailscale serve https:..." subcommand.
|
||||||
|
// It configures the serve config to forward HTTPS connections to the
|
||||||
|
// given source.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - tailscale serve https / http://localhost:3000
|
||||||
|
// - tailscale serve https:8443 /files/ /home/alice/shared-files/
|
||||||
|
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
|
||||||
|
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, source string) error {
|
||||||
h := new(ipn.HTTPHandler)
|
h := new(ipn.HTTPHandler)
|
||||||
|
|
||||||
switch args[1] {
|
ts, _, _ := strings.Cut(source, ":")
|
||||||
case "path":
|
switch {
|
||||||
|
case ts == "text":
|
||||||
|
text := strings.TrimPrefix(source, "text:")
|
||||||
|
if text == "" {
|
||||||
|
return errors.New("unable to serve; text cannot be an empty string")
|
||||||
|
}
|
||||||
|
h.Text = text
|
||||||
|
case isProxyTarget(source):
|
||||||
|
t, err := expandProxyTarget(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.Proxy = t
|
||||||
|
default: // assume path
|
||||||
if version.IsSandboxedMacOS() {
|
if version.IsSandboxedMacOS() {
|
||||||
// don't allow path serving for now on macOS (2022-11-15)
|
// don't allow path serving for now on macOS (2022-11-15)
|
||||||
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
|
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(args[2]) {
|
if !filepath.IsAbs(source) {
|
||||||
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
|
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
|
||||||
return flag.ErrHelp
|
return flag.ErrHelp
|
||||||
}
|
}
|
||||||
fi, err := os.Stat(args[2])
|
source = filepath.Clean(source)
|
||||||
|
fi, err := os.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
|
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
|
||||||
return flag.ErrHelp
|
return flag.ErrHelp
|
||||||
|
@ -265,21 +285,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||||
// for relative file links to work
|
// for relative file links to work
|
||||||
mount += "/"
|
mount += "/"
|
||||||
}
|
}
|
||||||
h.Path = args[2]
|
h.Path = source
|
||||||
case "proxy":
|
|
||||||
t, err := expandProxyTarget(args[2])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.Proxy = t
|
|
||||||
case "text":
|
|
||||||
if args[2] == "" {
|
|
||||||
return errors.New("unable to serve; text cannot be an empty string")
|
|
||||||
}
|
|
||||||
h.Text = args[2]
|
|
||||||
default:
|
|
||||||
fmt.Fprintf(os.Stderr, "error: unknown serve type %q\n\n", args[1])
|
|
||||||
return flag.ErrHelp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cursc, err := e.lc.GetServeConfig(ctx)
|
cursc, err := e.lc.GetServeConfig(ctx)
|
||||||
|
@ -294,7 +300,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
|
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||||
|
|
||||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||||
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
|
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
|
||||||
|
@ -333,12 +339,36 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error {
|
// isProxyTarget reports whether source is a valid proxy target.
|
||||||
srvPort, err := e.validateServePort()
|
func isProxyTarget(source string) bool {
|
||||||
if err != nil {
|
if strings.HasPrefix(source, "http://") ||
|
||||||
return err
|
strings.HasPrefix(source, "https://") ||
|
||||||
|
strings.HasPrefix(source, "https+insecure://") {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
srvPortStr := strconv.Itoa(int(srvPort))
|
// support "localhost:3000", for example
|
||||||
|
_, portStr, ok := strings.Cut(source, ":")
|
||||||
|
if ok && allNumeric(portStr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// allNumeric reports whether s only comprises of digits
|
||||||
|
// and has at least one digit.
|
||||||
|
func allNumeric(s string) bool {
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] < '0' || s[i] > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWebServeRemove removes a web handler from the serve config.
|
||||||
|
// The srvPort argument is the serving port and the mount argument is
|
||||||
|
// the mount point or registered path to remove.
|
||||||
|
func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mount string) error {
|
||||||
sc, err := e.lc.GetServeConfig(ctx)
|
sc, err := e.lc.GetServeConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -353,9 +383,9 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error
|
||||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||||
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, srvPortStr))
|
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||||
if !sc.WebHandlerExists(hp, mount) {
|
if !sc.WebHandlerExists(hp, mount) {
|
||||||
return errors.New("error: serve config does not exist")
|
return errors.New("error: handler does not exist")
|
||||||
}
|
}
|
||||||
// delete existing handler, then cascade delete if empty
|
// delete existing handler, then cascade delete if empty
|
||||||
delete(sc.Web[hp].Handlers, mount)
|
delete(sc.Web[hp].Handlers, mount)
|
||||||
|
@ -390,18 +420,11 @@ func cleanMountPoint(mount string) (string, error) {
|
||||||
return "", fmt.Errorf("invalid mount point %q", mount)
|
return "", fmt.Errorf("invalid mount point %q", mount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandProxyTarget(target string) (string, error) {
|
func expandProxyTarget(source string) (string, error) {
|
||||||
if allNumeric(target) {
|
if !strings.Contains(source, "://") {
|
||||||
p, err := strconv.ParseUint(target, 10, 16)
|
source = "http://" + source
|
||||||
if p == 0 || err != nil {
|
|
||||||
return "", fmt.Errorf("invalid port %q", target)
|
|
||||||
}
|
}
|
||||||
return "http://127.0.0.1:" + target, nil
|
u, err := url.ParseRequestURI(source)
|
||||||
}
|
|
||||||
if !strings.Contains(target, "://") {
|
|
||||||
target = "http://" + target
|
|
||||||
}
|
|
||||||
u, err := url.ParseRequestURI(target)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("parsing url: %w", err)
|
return "", fmt.Errorf("parsing url: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -411,9 +434,14 @@ func expandProxyTarget(target string) (string, error) {
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("must be a URL starting with http://, https://, or https+insecure://")
|
return "", fmt.Errorf("must be a URL starting with http://, https://, or https+insecure://")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port, err := strconv.ParseUint(u.Port(), 10, 16)
|
||||||
|
if port == 0 || err != nil {
|
||||||
|
return "", fmt.Errorf("invalid port %q: %w", u.Port(), err)
|
||||||
|
}
|
||||||
|
|
||||||
host := u.Hostname()
|
host := u.Hostname()
|
||||||
switch host {
|
switch host {
|
||||||
// TODO(shayne,bradfitz): do we want to do this?
|
|
||||||
case "localhost", "127.0.0.1":
|
case "localhost", "127.0.0.1":
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
default:
|
default:
|
||||||
|
@ -426,16 +454,111 @@ func expandProxyTarget(target string) (string, error) {
|
||||||
return url, nil
|
return url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func allNumeric(s string) bool {
|
// handleTCPServe handles the "tailscale serve tls-terminated-tcp:..." subcommand.
|
||||||
for i := 0; i < len(s); i++ {
|
// It configures the serve config to forward TCP connections to the
|
||||||
if s[i] < '0' || s[i] > '9' {
|
// given source.
|
||||||
return false
|
//
|
||||||
}
|
// Examples:
|
||||||
}
|
// - tailscale serve tcp:2222 tcp://localhost:22
|
||||||
return s != ""
|
// - tailscale serve tls-terminated-tcp:8443 tcp://localhost:8080
|
||||||
|
func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort uint16, dest string) error {
|
||||||
|
var terminateTLS bool
|
||||||
|
switch srcType {
|
||||||
|
case "tcp":
|
||||||
|
terminateTLS = false
|
||||||
|
case "tls-terminated-tcp":
|
||||||
|
terminateTLS = true
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n\n", dest)
|
||||||
|
return flag.ErrHelp
|
||||||
}
|
}
|
||||||
|
|
||||||
// runServeStatus prints the current serve config.
|
dstURL, err := url.Parse(dest)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
switch host {
|
||||||
|
case "localhost", "127.0.0.1":
|
||||||
|
// ok
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n", dest)
|
||||||
|
fmt.Fprint(os.Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", dstPortStr)
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
cursc, err := e.lc.GetServeConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sc := cursc.Clone() // nil if no config
|
||||||
|
if sc == nil {
|
||||||
|
sc = new(ipn.ServeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
fwdAddr := "127.0.0.1:" + dstPortStr
|
||||||
|
|
||||||
|
if sc.IsServingWeb(srcPort) {
|
||||||
|
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
|
||||||
|
|
||||||
|
dnsName, err := e.getSelfDNSName(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if terminateTLS {
|
||||||
|
sc.TCP[srcPort].TerminateTLS = dnsName
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(cursc, sc) {
|
||||||
|
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTCPServeRemove removes the TCP forwarding configuration for the
|
||||||
|
// given srvPort, or serving port.
|
||||||
|
func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
|
||||||
|
cursc, err := e.lc.GetServeConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sc := cursc.Clone() // nil if no config
|
||||||
|
if sc == nil {
|
||||||
|
sc = new(ipn.ServeConfig)
|
||||||
|
}
|
||||||
|
if sc.IsServingWeb(src) {
|
||||||
|
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
||||||
|
}
|
||||||
|
if ph := sc.GetTCPPortHandler(src); ph != nil {
|
||||||
|
delete(sc.TCP, src)
|
||||||
|
// clear map mostly for testing
|
||||||
|
if len(sc.TCP) == 0 {
|
||||||
|
sc.TCP = nil
|
||||||
|
}
|
||||||
|
return e.lc.SetServeConfig(ctx, sc)
|
||||||
|
}
|
||||||
|
return errors.New("error: serve config does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runServeStatus is the entry point for the "serve status"
|
||||||
|
// subcommand and prints the current serve config.
|
||||||
//
|
//
|
||||||
// Examples:
|
// Examples:
|
||||||
// - tailscale status
|
// - tailscale status
|
||||||
|
@ -454,6 +577,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||||
e.stdout().Write(j)
|
e.stdout().Write(j)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
printFunnelStatus(ctx)
|
||||||
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
|
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
|
||||||
printf("No serve config\n")
|
printf("No serve config\n")
|
||||||
return nil
|
return nil
|
||||||
|
@ -472,17 +596,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||||
printWebStatusTree(sc, hp)
|
printWebStatusTree(sc, hp)
|
||||||
printf("\n")
|
printf("\n")
|
||||||
}
|
}
|
||||||
// warn when funnel on without handlers
|
printFunnelWarning(sc)
|
||||||
for hp, a := range sc.AllowFunnel {
|
|
||||||
if !a {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, portStr, _ := net.SplitHostPort(string(hp))
|
|
||||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
|
||||||
if _, ok := sc.TCP[uint16(p)]; !ok {
|
|
||||||
printf("WARNING: funnel=on for %s, but no serve config\n", hp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,133 +680,3 @@ func elipticallyTruncate(s string, max int) string {
|
||||||
}
|
}
|
||||||
return s[:max-3] + "..."
|
return s[:max-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
// runServeTCP is the entry point for the "serve tcp" subcommand and
|
|
||||||
// manages the serve config for TCP forwarding.
|
|
||||||
//
|
|
||||||
// Examples:
|
|
||||||
// - tailscale serve tcp 5432
|
|
||||||
// - tailscale serve --serve-port=8443 tcp 4430
|
|
||||||
// - tailscale serve --serve-port=10000 tcp --terminate-tls 8080
|
|
||||||
func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
|
|
||||||
if len(args) != 1 {
|
|
||||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
|
||||||
return flag.ErrHelp
|
|
||||||
}
|
|
||||||
|
|
||||||
srvPort, err := e.validateServePort()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
portStr := args[0]
|
|
||||||
p, err := strconv.ParseUint(portStr, 10, 16)
|
|
||||||
if p == 0 || err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", portStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
cursc, err := e.lc.GetServeConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sc := cursc.Clone() // nil if no config
|
|
||||||
if sc == nil {
|
|
||||||
sc = new(ipn.ServeConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
fwdAddr := "127.0.0.1:" + portStr
|
|
||||||
|
|
||||||
if sc.IsServingWeb(srvPort) {
|
|
||||||
if e.remove {
|
|
||||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", srvPort)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srvPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.remove {
|
|
||||||
if ph := sc.GetTCPPortHandler(srvPort); ph != nil && ph.TCPForward == fwdAddr {
|
|
||||||
delete(sc.TCP, srvPort)
|
|
||||||
// clear map mostly for testing
|
|
||||||
if len(sc.TCP) == 0 {
|
|
||||||
sc.TCP = nil
|
|
||||||
}
|
|
||||||
return e.lc.SetServeConfig(ctx, sc)
|
|
||||||
}
|
|
||||||
return errors.New("error: serve config does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
|
|
||||||
|
|
||||||
dnsName, err := e.getSelfDNSName(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.terminateTLS {
|
|
||||||
sc.TCP[srvPort].TerminateTLS = dnsName
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(cursc, sc) {
|
|
||||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// runServeFunnel is the entry point for the "serve funnel" subcommand and
|
|
||||||
// manages turning on/off funnel. Funnel is off by default.
|
|
||||||
//
|
|
||||||
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
|
|
||||||
func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
|
||||||
if len(args) != 1 {
|
|
||||||
return flag.ErrHelp
|
|
||||||
}
|
|
||||||
|
|
||||||
srvPort, err := e.validateServePort()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
srvPortStr := strconv.Itoa(int(srvPort))
|
|
||||||
|
|
||||||
var on bool
|
|
||||||
switch args[0] {
|
|
||||||
case "on", "off":
|
|
||||||
on = args[0] == "on"
|
|
||||||
default:
|
|
||||||
return flag.ErrHelp
|
|
||||||
}
|
|
||||||
sc, err := e.lc.GetServeConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if sc == nil {
|
|
||||||
sc = new(ipn.ServeConfig)
|
|
||||||
}
|
|
||||||
st, err := e.getLocalClientStatus(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting client status: %w", err)
|
|
||||||
}
|
|
||||||
if err := ipn.CheckFunnelAccess(srvPort, st.Self.Capabilities); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
|
||||||
hp := ipn.HostPort(dnsName + ":" + srvPortStr)
|
|
||||||
if on == sc.AllowFunnel[hp] {
|
|
||||||
// Nothing to do.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if on {
|
|
||||||
mak.Set(&sc.AllowFunnel, hp, true)
|
|
||||||
} else {
|
|
||||||
delete(sc.AllowFunnel, hp)
|
|
||||||
// clear map mostly for testing
|
|
||||||
if len(sc.AllowFunnel) == 0 {
|
|
||||||
sc.AllowFunnel = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
@ -56,6 +57,8 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
want *ipn.ServeConfig // non-nil means we want a save of this value
|
want *ipn.ServeConfig // non-nil means we want a save of this value
|
||||||
wantErr func(error) (badErrMsg string) // nil means no error is wanted
|
wantErr func(error) (badErrMsg string) // nil means no error is wanted
|
||||||
line int // line number of addStep call, for error messages
|
line int // line number of addStep call, for error messages
|
||||||
|
|
||||||
|
debugBreak func()
|
||||||
}
|
}
|
||||||
var steps []step
|
var steps []step
|
||||||
add := func(s step) {
|
add := func(s step) {
|
||||||
|
@ -66,19 +69,19 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
// funnel
|
// funnel
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("funnel on"),
|
command: cmd("funnel 443 on"),
|
||||||
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
|
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("funnel on"),
|
command: cmd("funnel 443 on"),
|
||||||
want: nil, // nothing to save
|
want: nil, // nothing to save
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("funnel off"),
|
command: cmd("funnel 443 off"),
|
||||||
want: &ipn.ServeConfig{},
|
want: &ipn.ServeConfig{},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("funnel off"),
|
command: cmd("funnel 443 off"),
|
||||||
want: nil, // nothing to save
|
want: nil, // nothing to save
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
|
@ -89,27 +92,48 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
// https
|
// https
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ proxy 0"), // invalid port, too low
|
command: cmd("https:443 / http://localhost:0"), // invalid port, too low
|
||||||
wantErr: anyErr(),
|
wantErr: anyErr(),
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ proxy 65536"), // invalid port, too high
|
command: cmd("https:443 / http://localhost:65536"), // invalid port, too high
|
||||||
wantErr: anyErr(),
|
wantErr: anyErr(),
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ proxy somehost"), // invalid host
|
command: cmd("https:443 / http://somehost:3000"), // invalid host
|
||||||
wantErr: anyErr(),
|
wantErr: anyErr(),
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ proxy http://otherhost"), // invalid host
|
command: cmd("https:443 / httpz://127.0.0.1"), // invalid scheme
|
||||||
wantErr: anyErr(),
|
wantErr: anyErr(),
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{ // allow omitting port (default to 443)
|
||||||
command: cmd("/ proxy httpz://127.0.0.1"), // invalid scheme
|
command: cmd("https / http://localhost:3000"),
|
||||||
wantErr: anyErr(),
|
want: &ipn.ServeConfig{
|
||||||
|
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{ // support non Funnel port
|
||||||
|
command: cmd("https:9999 /abc http://localhost:3001"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {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"},
|
||||||
|
}},
|
||||||
|
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ proxy 3000"),
|
command: cmd("https:9999 /abc off"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -120,7 +144,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("--serve-port=8443 /abc proxy 3001"),
|
command: cmd("https:8443 /abc http://127.0.0.1:3001"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -134,7 +158,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("--serve-port=10000 / text hi"),
|
command: cmd("https:10000 / text:hi"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {HTTPS: true}},
|
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {HTTPS: true}},
|
||||||
|
@ -152,12 +176,12 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("--remove /foo"),
|
command: cmd("https:443 /foo off"),
|
||||||
want: nil, // nothing to save
|
want: nil, // nothing to save
|
||||||
wantErr: anyErr(),
|
wantErr: anyErr(),
|
||||||
}) // handler doesn't exist, so we get an error
|
}) // handler doesn't exist, so we get an error
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("--remove --serve-port=10000 /"),
|
command: cmd("https:10000 / off"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -171,7 +195,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("--remove /"),
|
command: cmd("https:443 / off"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -182,11 +206,11 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("--remove --serve-port=8443 /abc"),
|
command: cmd("https:8443 /abc off"),
|
||||||
want: &ipn.ServeConfig{},
|
want: &ipn.ServeConfig{},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{ // clean mount: "bar" becomes "/bar"
|
||||||
command: cmd("bar proxy https://127.0.0.1:8443"),
|
command: cmd("https:443 bar https://127.0.0.1:8443"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -197,12 +221,12 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("bar proxy https://127.0.0.1:8443"),
|
command: cmd("https:443 bar https://127.0.0.1:8443"),
|
||||||
want: nil, // nothing to save
|
want: nil, // nothing to save
|
||||||
})
|
})
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ proxy https+insecure://127.0.0.1:3001"),
|
command: cmd("https:443 / https+insecure://127.0.0.1:3001"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -214,7 +238,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
})
|
})
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/foo proxy localhost:3000"),
|
command: cmd("https:443 /foo localhost:3000"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -225,7 +249,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // test a second handler on the same port
|
add(step{ // test a second handler on the same port
|
||||||
command: cmd("--serve-port=8443 /foo proxy localhost:3000"),
|
command: cmd("https:8443 /foo localhost:3000"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -241,16 +265,35 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
|
|
||||||
// tcp
|
// tcp
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
|
add(step{ // must include scheme for tcp
|
||||||
|
command: cmd("tls-terminated-tcp:443 localhost:5432"),
|
||||||
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
|
})
|
||||||
|
add(step{ // !somehost, must be localhost or 127.0.0.1
|
||||||
|
command: cmd("tls-terminated-tcp:443 tcp://somehost:5432"),
|
||||||
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
|
})
|
||||||
|
add(step{ // bad target port, too low
|
||||||
|
command: cmd("tls-terminated-tcp:443 tcp://somehost:0"),
|
||||||
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
|
})
|
||||||
|
add(step{ // bad target port, too high
|
||||||
|
command: cmd("tls-terminated-tcp:443 tcp://somehost:65536"),
|
||||||
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("tcp 5432"),
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
443: {TCPForward: "127.0.0.1:5432"},
|
443: {
|
||||||
|
TCPForward: "127.0.0.1:5432",
|
||||||
|
TerminateTLS: "foo.test.ts.net",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("tcp -terminate-tls 8443"),
|
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
443: {
|
443: {
|
||||||
|
@ -261,11 +304,11 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("tcp -terminate-tls 8443"),
|
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
|
||||||
want: nil, // nothing to save
|
want: nil, // nothing to save
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("tcp --terminate-tls 8444"),
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:8444"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
443: {
|
443: {
|
||||||
|
@ -276,35 +319,41 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("tcp -terminate-tls=false 8445"),
|
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8445"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
443: {TCPForward: "127.0.0.1:8445"},
|
443: {
|
||||||
|
TCPForward: "127.0.0.1:8445",
|
||||||
|
TerminateTLS: "foo.test.ts.net",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("tcp 123"),
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:123"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
443: {TCPForward: "127.0.0.1:123"},
|
443: {
|
||||||
|
TCPForward: "127.0.0.1:123",
|
||||||
|
TerminateTLS: "foo.test.ts.net",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{ // handler doesn't exist, so we get an error
|
||||||
command: cmd("--remove tcp 321"),
|
command: cmd("tls-terminated-tcp:8443 off"),
|
||||||
wantErr: anyErr(),
|
wantErr: anyErr(),
|
||||||
}) // handler doesn't exist, so we get an error
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("--remove tcp 123"),
|
command: cmd("tls-terminated-tcp:443 off"),
|
||||||
want: &ipn.ServeConfig{},
|
want: &ipn.ServeConfig{},
|
||||||
})
|
})
|
||||||
|
|
||||||
// text
|
// text
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ text hello"),
|
command: cmd("https:443 / text:hello"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -325,7 +374,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
writeFile("foo", "this is foo")
|
writeFile("foo", "this is foo")
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ path " + filepath.Join(td, "foo")),
|
command: cmd("https:443 / " + filepath.Join(td, "foo")),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -338,7 +387,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
|
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
|
||||||
writeFile("subdir/file-a", "this is A")
|
writeFile("subdir/file-a", "this is A")
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/some/where path " + filepath.Join(td, "subdir/file-a")),
|
command: cmd("https:443 /some/where " + filepath.Join(td, "subdir/file-a")),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -349,13 +398,13 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{ // bad path
|
||||||
command: cmd("/ path missing"),
|
command: cmd("https:443 / bad/path"),
|
||||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
})
|
})
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ path " + filepath.Join(td, "subdir")),
|
command: cmd("https:443 / " + filepath.Join(td, "subdir")),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -366,14 +415,14 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("--remove /"),
|
command: cmd("https:443 / off"),
|
||||||
want: &ipn.ServeConfig{},
|
want: &ipn.ServeConfig{},
|
||||||
})
|
})
|
||||||
|
|
||||||
// combos
|
// combos
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("/ proxy 3000"),
|
command: cmd("https:443 / localhost:3000"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -384,7 +433,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("funnel on"),
|
command: cmd("funnel 443 on"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
|
@ -396,7 +445,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // serving on secondary port doesn't change funnel
|
add(step{ // serving on secondary port doesn't change funnel
|
||||||
command: cmd("--serve-port=8443 /bar proxy 3001"),
|
command: cmd("https:8443 /bar localhost:3001"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||||
|
@ -411,7 +460,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // turn funnel on for secondary port
|
add(step{ // turn funnel on for secondary port
|
||||||
command: cmd("--serve-port=8443 funnel on"),
|
command: cmd("funnel 8443 on"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||||
|
@ -426,7 +475,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // turn funnel off for primary port 443
|
add(step{ // turn funnel off for primary port 443
|
||||||
command: cmd("funnel off"),
|
command: cmd("funnel 443 off"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||||
|
@ -441,7 +490,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // remove secondary port
|
add(step{ // remove secondary port
|
||||||
command: cmd("--serve-port=8443 --remove /bar"),
|
command: cmd("https:8443 /bar off"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
|
@ -453,7 +502,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // start a tcp forwarder on 8443
|
add(step{ // start a tcp forwarder on 8443
|
||||||
command: cmd("--serve-port=8443 tcp 5432"),
|
command: cmd("tcp:8443 tcp://localhost:5432"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
|
||||||
|
@ -465,27 +514,27 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // remove primary port http handler
|
add(step{ // remove primary port http handler
|
||||||
command: cmd("--remove /"),
|
command: cmd("https:443 / off"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // remove tcp forwarder
|
add(step{ // remove tcp forwarder
|
||||||
command: cmd("--serve-port=8443 --remove tcp 5432"),
|
command: cmd("tls-terminated-tcp:8443 off"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // turn off funnel
|
add(step{ // turn off funnel
|
||||||
command: cmd("--serve-port=8443 funnel off"),
|
command: cmd("funnel 8443 off"),
|
||||||
want: &ipn.ServeConfig{},
|
want: &ipn.ServeConfig{},
|
||||||
})
|
})
|
||||||
|
|
||||||
// tricky steps
|
// tricky steps
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{ // a directory with a trailing slash mount point
|
add(step{ // a directory with a trailing slash mount point
|
||||||
command: cmd("/dir path " + filepath.Join(td, "subdir")),
|
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -496,7 +545,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // this should overwrite the previous one
|
add(step{ // this should overwrite the previous one
|
||||||
command: cmd("/dir path " + filepath.Join(td, "foo")),
|
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -508,7 +557,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
})
|
})
|
||||||
add(step{reset: true}) // reset and do the opposite
|
add(step{reset: true}) // reset and do the opposite
|
||||||
add(step{ // a file without a trailing slash mount point
|
add(step{ // a file without a trailing slash mount point
|
||||||
command: cmd("/dir path " + filepath.Join(td, "foo")),
|
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -519,7 +568,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // this should overwrite the previous one
|
add(step{ // this should overwrite the previous one
|
||||||
command: cmd("/dir path " + filepath.Join(td, "subdir")),
|
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -532,37 +581,24 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
|
|
||||||
// error states
|
// error states
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{ // make sure we can't add "tcp" as if it was a mount
|
|
||||||
command: cmd("tcp text foo"),
|
|
||||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
|
||||||
})
|
|
||||||
add(step{ // "/tcp" is fine though as a mount
|
|
||||||
command: cmd("/tcp text foo"),
|
|
||||||
want: &ipn.ServeConfig{
|
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
||||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
||||||
"/tcp": {Text: "foo"},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
add(step{reset: true})
|
|
||||||
add(step{ // tcp forward 5432 on serve port 443
|
add(step{ // tcp forward 5432 on serve port 443
|
||||||
command: cmd("tcp 5432"),
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
443: {TCPForward: "127.0.0.1:5432"},
|
443: {
|
||||||
|
TCPForward: "127.0.0.1:5432",
|
||||||
|
TerminateTLS: "foo.test.ts.net",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // try to start a web handler on the same port
|
add(step{ // try to start a web handler on the same port
|
||||||
command: cmd("/ proxy 3000"),
|
command: cmd("https:443 / localhost:3000"),
|
||||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
})
|
})
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{ // start a web handler on port 443
|
add(step{ // start a web handler on port 443
|
||||||
command: cmd("/ proxy 3000"),
|
command: cmd("https:443 / localhost:3000"),
|
||||||
want: &ipn.ServeConfig{
|
want: &ipn.ServeConfig{
|
||||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
@ -572,14 +608,17 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
add(step{ // try to start a tcp forwarder on the same serve port (443 default)
|
add(step{ // try to start a tcp forwarder on the same serve port
|
||||||
command: cmd("tcp 5432"),
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||||
wantErr: anyErr(),
|
wantErr: anyErr(),
|
||||||
})
|
})
|
||||||
|
|
||||||
lc := &fakeLocalServeClient{}
|
lc := &fakeLocalServeClient{}
|
||||||
// And now run the steps above.
|
// And now run the steps above.
|
||||||
for i, st := range steps {
|
for i, st := range steps {
|
||||||
|
if st.debugBreak != nil {
|
||||||
|
st.debugBreak()
|
||||||
|
}
|
||||||
if st.reset {
|
if st.reset {
|
||||||
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
|
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
|
||||||
lc.config = nil
|
lc.config = nil
|
||||||
|
@ -597,8 +636,16 @@ func TestServeConfigMutations(t *testing.T) {
|
||||||
testStdout: &stdout,
|
testStdout: &stdout,
|
||||||
}
|
}
|
||||||
lastCount := lc.setCount
|
lastCount := lc.setCount
|
||||||
cmd := newServeCommand(e)
|
var cmd *ffcli.Command
|
||||||
err := cmd.ParseAndRun(context.Background(), st.command)
|
var args []string
|
||||||
|
if st.command[0] == "funnel" {
|
||||||
|
cmd = newFunnelCommand(e)
|
||||||
|
args = st.command[1:]
|
||||||
|
} else {
|
||||||
|
cmd = newServeCommand(e)
|
||||||
|
args = st.command
|
||||||
|
}
|
||||||
|
err := cmd.ParseAndRun(context.Background(), args)
|
||||||
if flagOut.Len() > 0 {
|
if flagOut.Len() > 0 {
|
||||||
t.Logf("flag package output: %q", flagOut.Bytes())
|
t.Logf("flag package output: %q", flagOut.Bytes())
|
||||||
}
|
}
|
||||||
|
@ -689,7 +736,5 @@ func anyErr() func(error) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmd(s string) []string {
|
func cmd(s string) []string {
|
||||||
cmds := strings.Fields(s)
|
return strings.Fields(s)
|
||||||
fmt.Printf("cmd: %v", cmds)
|
|
||||||
return cmds
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,6 +258,7 @@ func printFunnelStatus(ctx context.Context) {
|
||||||
}
|
}
|
||||||
printf("# - %s\n", url)
|
printf("# - %s\n", url)
|
||||||
}
|
}
|
||||||
|
outln()
|
||||||
}
|
}
|
||||||
|
|
||||||
// isRunningOrStarting reports whether st is in state Running or Starting.
|
// isRunningOrStarting reports whether st is in state Running or Starting.
|
||||||
|
|
Loading…
Reference in New Issue