mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-16 03:31:39 +00:00
cmd/tailscale/cli: flesh out serve CLI and tests (#6304)
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
This commit is contained in:
parent
5f6d63936f
commit
a97369f097
@ -190,11 +190,10 @@ change in the future.
|
|||||||
if envknob.UseWIPCode() {
|
if envknob.UseWIPCode() {
|
||||||
rootCmd.Subcommands = append(rootCmd.Subcommands,
|
rootCmd.Subcommands = append(rootCmd.Subcommands,
|
||||||
idTokenCmd,
|
idTokenCmd,
|
||||||
serveCmd,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't advertise the debug command, but it exists.
|
// Don't advertise these commands, but they're still explicitly available.
|
||||||
switch {
|
switch {
|
||||||
case slices.Contains(args, "debug"):
|
case slices.Contains(args, "debug"):
|
||||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||||
@ -202,6 +201,8 @@ change in the future.
|
|||||||
rootCmd.Subcommands = append(rootCmd.Subcommands, loginCmd)
|
rootCmd.Subcommands = append(rootCmd.Subcommands, loginCmd)
|
||||||
case slices.Contains(args, "switch"):
|
case slices.Contains(args, "switch"):
|
||||||
rootCmd.Subcommands = append(rootCmd.Subcommands, switchCmd)
|
rootCmd.Subcommands = append(rootCmd.Subcommands, switchCmd)
|
||||||
|
case slices.Contains(args, "serve"):
|
||||||
|
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
|
||||||
}
|
}
|
||||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||||
|
@ -7,14 +7,27 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
|
"tailscale.com/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serveCmd = newServeCommand(&serveEnv{})
|
var serveCmd = newServeCommand(&serveEnv{})
|
||||||
@ -22,31 +35,80 @@ var serveCmd = newServeCommand(&serveEnv{})
|
|||||||
// newServeCommand returns a new "serve" subcommand using e as its environmment.
|
// newServeCommand returns a new "serve" subcommand using e as its environmment.
|
||||||
func newServeCommand(e *serveEnv) *ffcli.Command {
|
func newServeCommand(e *serveEnv) *ffcli.Command {
|
||||||
return &ffcli.Command{
|
return &ffcli.Command{
|
||||||
Name: "serve",
|
Name: "serve",
|
||||||
ShortHelp: "TODO",
|
ShortHelp: "[ALPHA] Serve from your Tailscale node",
|
||||||
ShortUsage: "serve {show-config|https|tcp|ingress} <args>",
|
ShortUsage: strings.TrimSpace(`
|
||||||
LongHelp: "", // TODO
|
serve [flags] <mount-point> {proxy|path|text} <arg>
|
||||||
Exec: e.runServe,
|
serve [flags] <sub-command> [sub-flags] <args>`),
|
||||||
FlagSet: e.newFlags("serve", func(fs *flag.FlagSet) {}),
|
LongHelp: strings.TrimSpace(`
|
||||||
|
*** ALPHA; all of this is subject to change ***
|
||||||
|
|
||||||
|
The 'tailscale serve' set of commands allows you to serve
|
||||||
|
content and local servers from your Tailscale node to
|
||||||
|
your tailnet.
|
||||||
|
|
||||||
|
You can also choose to enable the Tailscale Funnel with:
|
||||||
|
'tailscale serve funnel on'. Funnel allows you to publish
|
||||||
|
a 'tailscale serve' server publicly, open to the entire
|
||||||
|
internet. See https://tailscale.com/funnel.
|
||||||
|
|
||||||
|
EXAMPLES
|
||||||
|
- To proxy requests to a web server at 127.0.0.1:3000:
|
||||||
|
$ tailscale serve / proxy 3000
|
||||||
|
|
||||||
|
- To serve a single file or a directory of files:
|
||||||
|
$ tailscale serve / path /home/alice/blog/index.html
|
||||||
|
$ tailscale serve /images/ path /home/alice/blog/images
|
||||||
|
|
||||||
|
- To serve simple static text:
|
||||||
|
$ tailscale serve / text "Hello, world!"
|
||||||
|
`),
|
||||||
|
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,
|
||||||
Subcommands: []*ffcli.Command{
|
Subcommands: []*ffcli.Command{
|
||||||
{
|
{
|
||||||
Name: "show-config",
|
Name: "status",
|
||||||
Exec: e.runServeShowConfig,
|
Exec: e.runServeStatus,
|
||||||
ShortHelp: "show current serve config",
|
ShortHelp: "show current serve status",
|
||||||
|
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
|
||||||
|
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||||
|
}),
|
||||||
|
UsageFunc: usageFunc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "tcp",
|
Name: "tcp",
|
||||||
Exec: e.runServeTCP,
|
Exec: e.runServeTCP,
|
||||||
ShortHelp: "add or remove a TCP port forward",
|
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 --terminate-tls tcp 5432",
|
||||||
|
}, "\n"),
|
||||||
FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
|
FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
|
||||||
fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
|
fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
|
||||||
}),
|
}),
|
||||||
|
UsageFunc: usageFunc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "ingress",
|
Name: "funnel",
|
||||||
Exec: e.runServeIngress,
|
Exec: e.runServeFunnel,
|
||||||
ShortHelp: "enable or disable ingress",
|
ShortUsage: "funnel [flags] {on|off}",
|
||||||
FlagSet: e.newFlags("serve-ingress", func(fs *flag.FlagSet) {}),
|
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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -58,13 +120,44 @@ func newServeCommand(e *serveEnv) *ffcli.Command {
|
|||||||
// 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
|
terminateTLS bool
|
||||||
|
remove bool // remove a serve config
|
||||||
|
json bool // output JSON (status only for now)
|
||||||
|
|
||||||
// optional stuff for tests:
|
// optional stuff for tests:
|
||||||
testFlagOut io.Writer
|
testFlagOut io.Writer
|
||||||
testGetServeConfig func(context.Context) (*ipn.ServeConfig, error)
|
testGetServeConfig func(context.Context) (*ipn.ServeConfig, error)
|
||||||
testSetServeConfig func(context.Context, *ipn.ServeConfig) error
|
testSetServeConfig func(context.Context, *ipn.ServeConfig) error
|
||||||
testStdout io.Writer
|
testGetLocalClientStatus func(context.Context) (*ipnstate.Status, error)
|
||||||
|
testStdout io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *serveEnv) getSelfDNSName(ctx context.Context) (string, error) {
|
||||||
|
st, err := e.getLocalClientStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("getting client status: %w", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(st.Self.DNSName, "."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status, error) {
|
||||||
|
if e.testGetLocalClientStatus != nil {
|
||||||
|
return e.testGetLocalClientStatus(ctx)
|
||||||
|
}
|
||||||
|
st, err := localClient.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fixTailscaledConnectError(err)
|
||||||
|
}
|
||||||
|
description, ok := isRunningOrStarting(st)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", description)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if st.Self == nil {
|
||||||
|
return nil, errors.New("no self node")
|
||||||
|
}
|
||||||
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.FlagSet {
|
func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.FlagSet {
|
||||||
@ -101,7 +194,39 @@ func (e *serveEnv) stdout() io.Writer {
|
|||||||
return os.Stdout
|
return os.Stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
// make sure e.servePort is 443, 8443 or 10000
|
||||||
|
if port != 443 && port != 8443 && port != 10000 {
|
||||||
|
return 0, fmt.Errorf("serve-port %d is invalid; must be 443, 8443 or 10000", e.servePort)
|
||||||
|
}
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runServe is the entry point for the "serve" subcommand, managing Web
|
||||||
|
// serve config types like proxy, path, and text.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - tailscale serve / proxy 3000
|
||||||
|
// - tailscale serve /images/ path /var/www/images/
|
||||||
|
// - tailscale --serve-port=10000 serve /motd.txt text "Hello, world!"
|
||||||
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
srvPort, err := e.validateServePort()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srvPortStr := strconv.Itoa(int(srvPort))
|
||||||
|
|
||||||
// Undocumented debug command (not using ffcli subcommands) to set raw
|
// Undocumented debug command (not using ffcli subcommands) to set raw
|
||||||
// configs from stdin for now (2022-11-13).
|
// configs from stdin for now (2022-11-13).
|
||||||
if len(args) == 1 && args[0] == "set-raw" {
|
if len(args) == 1 && args[0] == "set-raw" {
|
||||||
@ -115,31 +240,471 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
|||||||
}
|
}
|
||||||
return localClient.SetServeConfig(ctx, sc)
|
return localClient.SetServeConfig(ctx, sc)
|
||||||
}
|
}
|
||||||
panic("TODO")
|
|
||||||
|
if !(len(args) == 3 || (e.remove && len(args) >= 1)) {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
mount, err := cleanMountPoint(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.remove {
|
||||||
|
return e.handleWebServeRemove(ctx, mount)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := new(ipn.HTTPHandler)
|
||||||
|
|
||||||
|
switch args[1] {
|
||||||
|
case "path":
|
||||||
|
if version.IsSandboxedMacOS() {
|
||||||
|
// don't allow path serving for now on macOS (2022-11-15)
|
||||||
|
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(args[2]) {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
fi, err := os.Stat(args[2])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
if fi.IsDir() && !strings.HasSuffix(mount, "/") {
|
||||||
|
// dir mount points must end in /
|
||||||
|
// for relative file links to work
|
||||||
|
mount += "/"
|
||||||
|
}
|
||||||
|
h.Path = args[2]
|
||||||
|
case "proxy":
|
||||||
|
t, err := expandProxyTarget(args[2])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.Proxy = t
|
||||||
|
case "text":
|
||||||
|
h.Text = args[2]
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "error: unknown serve type %q\n\n", args[1])
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
cursc, err := e.getServeConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sc := cursc.Clone() // nil if no config
|
||||||
|
if sc == nil {
|
||||||
|
sc = new(ipn.ServeConfig)
|
||||||
|
}
|
||||||
|
dnsName, err := e.getSelfDNSName(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
|
||||||
|
|
||||||
|
if isTCPForwardingOnPort(sc, srvPort) {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: true})
|
||||||
|
|
||||||
|
if _, ok := sc.Web[hp]; !ok {
|
||||||
|
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
|
||||||
|
}
|
||||||
|
mak.Set(&sc.Web[hp].Handlers, mount, h)
|
||||||
|
|
||||||
|
for k, v := range sc.Web[hp].Handlers {
|
||||||
|
if v == h {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If the new mount point ends in / and another mount point
|
||||||
|
// shares the same prefix, remove the other handler.
|
||||||
|
// (e.g. /foo/ overwrites /foo)
|
||||||
|
// The opposite example is also handled.
|
||||||
|
m1 := strings.TrimSuffix(mount, "/")
|
||||||
|
m2 := strings.TrimSuffix(k, "/")
|
||||||
|
if m1 == m2 {
|
||||||
|
delete(sc.Web[hp].Handlers, k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(cursc, sc) {
|
||||||
|
if err := e.setServeConfig(ctx, sc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *serveEnv) runServeShowConfig(ctx context.Context, args []string) error {
|
func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error {
|
||||||
|
srvPort, err := e.validateServePort()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srvPortStr := strconv.Itoa(int(srvPort))
|
||||||
sc, err := e.getServeConfig(ctx)
|
sc, err := e.getServeConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
j, err := json.MarshalIndent(sc, "", " ")
|
if sc == nil {
|
||||||
|
return errors.New("error: serve config does not exist")
|
||||||
|
}
|
||||||
|
dnsName, err := e.getSelfDNSName(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
j = append(j, '\n')
|
if isTCPForwardingOnPort(sc, srvPort) {
|
||||||
e.stdout().Write(j)
|
return errors.New("cannot remove web handler; currently serving TCP")
|
||||||
|
}
|
||||||
|
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
|
||||||
|
if !httpHandlerExists(sc, hp, mount) {
|
||||||
|
return errors.New("error: serve config does not exist")
|
||||||
|
}
|
||||||
|
// delete existing handler, then cascade delete if empty
|
||||||
|
delete(sc.Web[hp].Handlers, mount)
|
||||||
|
if len(sc.Web[hp].Handlers) == 0 {
|
||||||
|
delete(sc.Web, hp)
|
||||||
|
delete(sc.TCP, srvPort)
|
||||||
|
}
|
||||||
|
// clear empty maps mostly for testing
|
||||||
|
if len(sc.Web) == 0 {
|
||||||
|
sc.Web = nil
|
||||||
|
}
|
||||||
|
if len(sc.TCP) == 0 {
|
||||||
|
sc.TCP = nil
|
||||||
|
}
|
||||||
|
if err := e.setServeConfig(ctx, sc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
|
func httpHandlerExists(sc *ipn.ServeConfig, hp ipn.HostPort, mount string) bool {
|
||||||
panic("TODO")
|
h := getHTTPHandler(sc, hp, mount)
|
||||||
|
return h != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *serveEnv) runServeIngress(ctx context.Context, args []string) error {
|
func getHTTPHandler(sc *ipn.ServeConfig, hp ipn.HostPort, mount string) *ipn.HTTPHandler {
|
||||||
|
if sc != nil && sc.Web[hp] != nil {
|
||||||
|
return sc.Web[hp].Handlers[mount]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanMountPoint(mount string) (string, error) {
|
||||||
|
if mount == "" {
|
||||||
|
return "", errors.New("mount point cannot be empty")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(mount, "/") {
|
||||||
|
mount = "/" + mount
|
||||||
|
}
|
||||||
|
c := path.Clean(mount)
|
||||||
|
if mount == c || mount == c+"/" {
|
||||||
|
return mount, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("invalid mount point %q", mount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandProxyTarget(target string) (string, error) {
|
||||||
|
if allNumeric(target) {
|
||||||
|
p, err := strconv.ParseUint(target, 10, 16)
|
||||||
|
if p == 0 || err != nil {
|
||||||
|
return "", fmt.Errorf("invalid port %q", target)
|
||||||
|
}
|
||||||
|
return "http://127.0.0.1:" + target, nil
|
||||||
|
}
|
||||||
|
if !strings.Contains(target, "://") {
|
||||||
|
target = "http://" + target
|
||||||
|
}
|
||||||
|
u, err := url.ParseRequestURI(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parsing url: %w", err)
|
||||||
|
}
|
||||||
|
switch u.Scheme {
|
||||||
|
case "http", "https", "https+insecure":
|
||||||
|
// ok
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("must be a URL starting with http://, https://, or https+insecure://")
|
||||||
|
}
|
||||||
|
host := u.Hostname()
|
||||||
|
switch host {
|
||||||
|
// TODO(shayne,bradfitz): do we want to do this?
|
||||||
|
case "localhost", "127.0.0.1":
|
||||||
|
host = "127.0.0.1"
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("only localhost or 127.0.0.1 proxies are currently supported")
|
||||||
|
}
|
||||||
|
url := u.Scheme + "://" + host
|
||||||
|
if u.Port() != "" {
|
||||||
|
url += ":" + u.Port()
|
||||||
|
}
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTCPForwardingAny checks if any TCP port is being forwarded.
|
||||||
|
func isTCPForwardingAny(sc *ipn.ServeConfig) bool {
|
||||||
|
if sc == nil || len(sc.TCP) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, h := range sc.TCP {
|
||||||
|
if h.TCPForward != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTCPForwardingOnPort checks serve config to see if
|
||||||
|
// we're specifically forwarding TCP on the given port.
|
||||||
|
func isTCPForwardingOnPort(sc *ipn.ServeConfig, port uint16) bool {
|
||||||
|
if sc == nil || sc.TCP[port] == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !sc.TCP[port].HTTPS
|
||||||
|
}
|
||||||
|
|
||||||
|
// isServingWeb checks serve config to see if
|
||||||
|
// we're serving a web handler on the given port.
|
||||||
|
func isServingWeb(sc *ipn.ServeConfig, port uint16) bool {
|
||||||
|
if sc == nil || sc.Web == nil || sc.TCP == nil ||
|
||||||
|
sc.TCP[port] == nil || sc.TCP[port].HTTPS == false {
|
||||||
|
// not listening on port
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func allNumeric(s string) bool {
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] < '0' || s[i] > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// runServeStatus prints the current serve config.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - tailscale status
|
||||||
|
// - tailscale status --json
|
||||||
|
func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||||
|
sc, err := e.getServeConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.json {
|
||||||
|
j, err := json.MarshalIndent(sc, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
j = append(j, '\n')
|
||||||
|
e.stdout().Write(j)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
|
||||||
|
printf("No serve config\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
st, err := e.getLocalClientStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isTCPForwardingAny(sc) {
|
||||||
|
if err := printTCPStatusTree(ctx, sc, st); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printf("\n")
|
||||||
|
}
|
||||||
|
for hp := range sc.Web {
|
||||||
|
printWebStatusTree(sc, hp)
|
||||||
|
printf("\n")
|
||||||
|
}
|
||||||
|
// warn when funnel on without handlers
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status) error {
|
||||||
|
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||||
|
for p, h := range sc.TCP {
|
||||||
|
if h.TCPForward == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(p))))
|
||||||
|
tlsStatus := "TLS over TCP"
|
||||||
|
if h.TerminateTLS != "" {
|
||||||
|
tlsStatus = "TLS terminated"
|
||||||
|
}
|
||||||
|
fStatus := "tailnet only"
|
||||||
|
if isFunnelOn(sc, hp) {
|
||||||
|
fStatus = "Funnel on"
|
||||||
|
}
|
||||||
|
printf("|-- tcp://%s (%s, %s)\n", hp, tlsStatus, fStatus)
|
||||||
|
for _, a := range st.TailscaleIPs {
|
||||||
|
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(p)))
|
||||||
|
printf("|-- tcp://%s\n", ipp)
|
||||||
|
}
|
||||||
|
printf("|--> tcp://%s\n", h.TCPForward)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
|
||||||
|
if sc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fStatus := "tailnet only"
|
||||||
|
if isFunnelOn(sc, hp) {
|
||||||
|
fStatus = "Funnel on"
|
||||||
|
}
|
||||||
|
host, portStr, _ := net.SplitHostPort(string(hp))
|
||||||
|
if portStr == "443" {
|
||||||
|
printf("https://%s (%s)\n", host, fStatus)
|
||||||
|
} else {
|
||||||
|
printf("https://%s:%s (%s)\n", host, portStr, fStatus)
|
||||||
|
}
|
||||||
|
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
|
||||||
|
switch {
|
||||||
|
case h.Path != "":
|
||||||
|
return "path", h.Path
|
||||||
|
case h.Proxy != "":
|
||||||
|
return "proxy", h.Proxy
|
||||||
|
case h.Text != "":
|
||||||
|
return "text", "\"" + elipticallyTruncate(h.Text, 20) + "\""
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var mounts []string
|
||||||
|
for k := range sc.Web[hp].Handlers {
|
||||||
|
mounts = append(mounts, k)
|
||||||
|
}
|
||||||
|
sort.Slice(mounts, func(i, j int) bool {
|
||||||
|
return len(mounts[i]) < len(mounts[j])
|
||||||
|
})
|
||||||
|
maxLen := len(mounts[len(mounts)-1])
|
||||||
|
|
||||||
|
for _, m := range mounts {
|
||||||
|
h := sc.Web[hp].Handlers[m]
|
||||||
|
t, d := srvTypeAndDesc(h)
|
||||||
|
printf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func elipticallyTruncate(s string, max int) string {
|
||||||
|
if len(s) <= max {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
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-port=8443 tcp 4430
|
||||||
|
// - tailscale --serve-port=10000 --terminate-tls tcp 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.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 e.remove {
|
||||||
|
if isServingWeb(sc, srvPort) {
|
||||||
|
return errors.New("cannot remove TCP port; currently serving web")
|
||||||
|
}
|
||||||
|
if sc.TCP != nil && sc.TCP[srvPort] != nil &&
|
||||||
|
sc.TCP[srvPort].TCPForward == fwdAddr {
|
||||||
|
delete(sc.TCP, srvPort)
|
||||||
|
// clear map mostly for testing
|
||||||
|
if len(sc.TCP) == 0 {
|
||||||
|
sc.TCP = nil
|
||||||
|
}
|
||||||
|
return e.setServeConfig(ctx, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("error: serve config does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isServingWeb(sc, srvPort) {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: cannot serve TCP; already serving Web\n\n")
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 {
|
if len(args) != 1 {
|
||||||
return flag.ErrHelp
|
return flag.ErrHelp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
srvPort, err := e.validateServePort()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srvPortStr := strconv.Itoa(int(srvPort))
|
||||||
|
|
||||||
var on bool
|
var on bool
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "on", "off":
|
case "on", "off":
|
||||||
@ -151,9 +716,17 @@ func (e *serveEnv) runServeIngress(ctx context.Context, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var key ipn.HostPort = "foo:123" // TODO(bradfitz,shayne): fix
|
st, err := e.getLocalClientStatus(ctx)
|
||||||
if on && sc != nil && sc.AllowIngress[key] ||
|
if err != nil {
|
||||||
!on && (sc == nil || !sc.AllowIngress[key]) {
|
return fmt.Errorf("getting client status: %w", err)
|
||||||
|
}
|
||||||
|
if !slices.Contains(st.Self.Capabilities, tailcfg.NodeAttrFunnel) {
|
||||||
|
return errors.New("Funnel not available. See https://tailscale.com/s/no-funnel")
|
||||||
|
}
|
||||||
|
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||||
|
hp := ipn.HostPort(dnsName + ":" + srvPortStr)
|
||||||
|
if on && sc != nil && sc.AllowFunnel[hp] ||
|
||||||
|
!on && (sc == nil || !sc.AllowFunnel[hp]) {
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -161,9 +734,20 @@ func (e *serveEnv) runServeIngress(ctx context.Context, args []string) error {
|
|||||||
sc = &ipn.ServeConfig{}
|
sc = &ipn.ServeConfig{}
|
||||||
}
|
}
|
||||||
if on {
|
if on {
|
||||||
mak.Set(&sc.AllowIngress, "foo:123", true)
|
mak.Set(&sc.AllowFunnel, hp, true)
|
||||||
} else {
|
} else {
|
||||||
delete(sc.AllowIngress, "foo:123")
|
delete(sc.AllowFunnel, hp)
|
||||||
|
// clear map mostly for testing
|
||||||
|
if len(sc.AllowFunnel) == 0 {
|
||||||
|
sc.AllowFunnel = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return e.setServeConfig(ctx, sc)
|
if err := e.setServeConfig(ctx, sc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFunnelOn(sc *ipn.ServeConfig, hp ipn.HostPort) bool {
|
||||||
|
return sc != nil && sc.AllowFunnel[hp]
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,46 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestCleanMountPoint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
mount string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"foo", "/foo", false}, // missing prefix
|
||||||
|
{"/foo/", "/foo/", false}, // keep trailing slash
|
||||||
|
{"////foo", "", true}, // too many slashes
|
||||||
|
{"/foo//", "", true}, // too many slashes
|
||||||
|
{"", "", true}, // empty
|
||||||
|
{"https://tailscale.com", "", true}, // not a path
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
mp, err := cleanMountPoint(tt.mount)
|
||||||
|
if err != nil && tt.wantErr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mp != tt.want {
|
||||||
|
t.Fatalf("got %q, want %q", mp, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeConfigMutations(t *testing.T) {
|
func TestServeConfigMutations(t *testing.T) {
|
||||||
// Stateful mutations, starting from an empty config.
|
// Stateful mutations, starting from an empty config.
|
||||||
type step struct {
|
type step struct {
|
||||||
@ -32,25 +64,521 @@ func TestServeConfigMutations(t *testing.T) {
|
|||||||
steps = append(steps, s)
|
steps = append(steps, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// funnel
|
||||||
add(step{reset: true})
|
add(step{reset: true})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("ingress on"),
|
command: cmd("funnel on"),
|
||||||
want: &ipn.ServeConfig{AllowIngress: map[ipn.HostPort]bool{"foo:123": true}},
|
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("ingress on"),
|
command: cmd("funnel on"),
|
||||||
want: nil, // nothing to save
|
want: nil, // nothing to save
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("ingress off"),
|
command: cmd("funnel off"),
|
||||||
want: &ipn.ServeConfig{AllowIngress: map[ipn.HostPort]bool{}},
|
want: &ipn.ServeConfig{},
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("ingress off"),
|
command: cmd("funnel off"),
|
||||||
want: nil, // nothing to save
|
want: nil, // nothing to save
|
||||||
})
|
})
|
||||||
add(step{
|
add(step{
|
||||||
command: cmd("ingress"),
|
command: cmd("funnel"),
|
||||||
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// https
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ proxy 0"), // invalid port, too low
|
||||||
|
wantErr: anyErr(),
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ proxy 65536"), // invalid port, too high
|
||||||
|
wantErr: anyErr(),
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ proxy somehost"), // invalid host
|
||||||
|
wantErr: anyErr(),
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ proxy http://otherhost"), // invalid host
|
||||||
|
wantErr: anyErr(),
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ proxy httpz://127.0.0.1"), // invalid scheme
|
||||||
|
wantErr: anyErr(),
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ proxy 3000"),
|
||||||
|
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{
|
||||||
|
command: cmd("--serve-port=9999 /abc proxy 3001"),
|
||||||
|
wantErr: anyErr(),
|
||||||
|
}) // invalid port
|
||||||
|
add(step{
|
||||||
|
command: cmd("--serve-port=8443 /abc proxy 3001"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {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:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("--serve-port=10000 / text hi"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {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:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
"foo.test.ts.net:10000": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Text: "hi"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("--remove /foo"),
|
||||||
|
want: nil, // nothing to save
|
||||||
|
wantErr: anyErr(),
|
||||||
|
}) // handler doesn't exist, so we get an error
|
||||||
|
add(step{
|
||||||
|
command: cmd("--remove --serve-port=10000 /"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {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:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("--remove /"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("--remove --serve-port=8443 /abc"),
|
||||||
|
want: &ipn.ServeConfig{},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("bar proxy https://127.0.0.1:8443"),
|
||||||
|
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{
|
||||||
|
"/bar": {Proxy: "https://127.0.0.1:8443"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("bar proxy https://127.0.0.1:8443"),
|
||||||
|
want: nil, // nothing to save
|
||||||
|
})
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ proxy https+insecure://127.0.0.1:3001"),
|
||||||
|
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: "https+insecure://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/foo proxy localhost:3000"),
|
||||||
|
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{
|
||||||
|
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // test a second handler on the same port
|
||||||
|
command: cmd("--serve-port=8443 /foo proxy localhost:3000"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||||
|
}},
|
||||||
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// tcp
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{
|
||||||
|
command: cmd("tcp 5432"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {TCPForward: "127.0.0.1:5432"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("tcp -terminate-tls 8443"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {
|
||||||
|
TCPForward: "127.0.0.1:8443",
|
||||||
|
TerminateTLS: "foo.test.ts.net",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("tcp -terminate-tls 8443"),
|
||||||
|
want: nil, // nothing to save
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("tcp --terminate-tls 8444"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {
|
||||||
|
TCPForward: "127.0.0.1:8444",
|
||||||
|
TerminateTLS: "foo.test.ts.net",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("tcp -terminate-tls=false 8445"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {TCPForward: "127.0.0.1:8445"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{
|
||||||
|
command: cmd("tcp 123"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {TCPForward: "127.0.0.1:123"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("--remove tcp 321"),
|
||||||
|
wantErr: anyErr(),
|
||||||
|
}) // handler doesn't exist, so we get an error
|
||||||
|
add(step{
|
||||||
|
command: cmd("--remove tcp 123"),
|
||||||
|
want: &ipn.ServeConfig{},
|
||||||
|
})
|
||||||
|
|
||||||
|
// text
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ text hello"),
|
||||||
|
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{
|
||||||
|
"/": {Text: "hello"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// path
|
||||||
|
td := t.TempDir()
|
||||||
|
writeFile := func(suffix, contents string) {
|
||||||
|
if err := os.WriteFile(filepath.Join(td, suffix), []byte(contents), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(step{reset: true})
|
||||||
|
writeFile("foo", "this is foo")
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ path " + filepath.Join(td, "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{
|
||||||
|
"/": {Path: filepath.Join(td, "foo")},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
|
||||||
|
writeFile("subdir/file-a", "this is A")
|
||||||
|
add(step{
|
||||||
|
command: cmd("/some/where path " + filepath.Join(td, "subdir/file-a")),
|
||||||
|
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{
|
||||||
|
"/": {Path: filepath.Join(td, "foo")},
|
||||||
|
"/some/where": {Path: filepath.Join(td, "subdir/file-a")},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ path missing"),
|
||||||
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
|
})
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ path " + filepath.Join(td, "subdir")),
|
||||||
|
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{
|
||||||
|
"/": {Path: filepath.Join(td, "subdir/")},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{
|
||||||
|
command: cmd("--remove /"),
|
||||||
|
want: &ipn.ServeConfig{},
|
||||||
|
})
|
||||||
|
|
||||||
|
// combos
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{
|
||||||
|
command: cmd("/ proxy 3000"),
|
||||||
|
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{
|
||||||
|
command: cmd("funnel on"),
|
||||||
|
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{ // serving on secondary port doesn't change funnel
|
||||||
|
command: cmd("--serve-port=8443 /bar proxy 3001"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {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:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // turn funnel on for secondary port
|
||||||
|
command: cmd("--serve-port=8443 funnel on"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
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}},
|
||||||
|
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:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // turn funnel off for primary port 443
|
||||||
|
command: cmd("funnel off"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {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:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // remove secondary port
|
||||||
|
command: cmd("--serve-port=8443 --remove /bar"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": 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{ // start a tcp forwarder on 8443
|
||||||
|
command: cmd("--serve-port=8443 tcp 5432"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
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"}},
|
||||||
|
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{ // remove primary port http handler
|
||||||
|
command: cmd("--remove /"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // remove tcp forwarder
|
||||||
|
command: cmd("--serve-port=8443 --remove tcp 5432"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // turn off funnel
|
||||||
|
command: cmd("--serve-port=8443 funnel off"),
|
||||||
|
want: &ipn.ServeConfig{},
|
||||||
|
})
|
||||||
|
|
||||||
|
// tricky steps
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{ // a directory with a trailing slash mount point
|
||||||
|
command: cmd("/dir path " + filepath.Join(td, "subdir")),
|
||||||
|
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{
|
||||||
|
"/dir/": {Path: filepath.Join(td, "subdir/")},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // this should overwrite the previous one
|
||||||
|
command: cmd("/dir path " + filepath.Join(td, "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{
|
||||||
|
"/dir": {Path: filepath.Join(td, "foo")},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{reset: true}) // reset and do the opposite
|
||||||
|
add(step{ // a file without a trailing slash mount point
|
||||||
|
command: cmd("/dir path " + filepath.Join(td, "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{
|
||||||
|
"/dir": {Path: filepath.Join(td, "foo")},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // this should overwrite the previous one
|
||||||
|
command: cmd("/dir path " + filepath.Join(td, "subdir")),
|
||||||
|
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{
|
||||||
|
"/dir/": {Path: filepath.Join(td, "subdir/")},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// error states
|
||||||
|
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
|
||||||
|
command: cmd("tcp 5432"),
|
||||||
|
want: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {TCPForward: "127.0.0.1:5432"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
add(step{ // try to start a web handler on the same port
|
||||||
|
command: cmd("/ proxy 3000"),
|
||||||
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
|
})
|
||||||
|
add(step{reset: true})
|
||||||
|
add(step{ // start a web handler on port 443
|
||||||
|
command: cmd("/ proxy 3000"),
|
||||||
|
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{ // try to start a tcp forwarder on the same port
|
||||||
|
command: cmd("tcp 5432"),
|
||||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -72,6 +600,14 @@ func TestServeConfigMutations(t *testing.T) {
|
|||||||
e := &serveEnv{
|
e := &serveEnv{
|
||||||
testFlagOut: &flagOut,
|
testFlagOut: &flagOut,
|
||||||
testStdout: &stdout,
|
testStdout: &stdout,
|
||||||
|
testGetLocalClientStatus: func(context.Context) (*ipnstate.Status, error) {
|
||||||
|
return &ipnstate.Status{
|
||||||
|
Self: &ipnstate.PeerStatus{
|
||||||
|
DNSName: "foo.test.ts.net",
|
||||||
|
Capabilities: []string{tailcfg.NodeAttrFunnel},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
testGetServeConfig: func(context.Context) (*ipn.ServeConfig, error) {
|
testGetServeConfig: func(context.Context) (*ipn.ServeConfig, error) {
|
||||||
return current, nil
|
return current, nil
|
||||||
},
|
},
|
||||||
@ -100,6 +636,11 @@ func TestServeConfigMutations(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(newState, st.want) {
|
if !reflect.DeepEqual(newState, st.want) {
|
||||||
t.Fatalf("[%d] %v: bad state. got:\n%s\n\nwant:\n%s\n",
|
t.Fatalf("[%d] %v: bad state. got:\n%s\n\nwant:\n%s\n",
|
||||||
i, st.command, asJSON(newState), asJSON(st.want))
|
i, st.command, asJSON(newState), asJSON(st.want))
|
||||||
|
// NOTE: asJSON will omit empty fields, which might make
|
||||||
|
// result in bad state got/want diffs being the same, even
|
||||||
|
// though the actual state is different. Use below to debug:
|
||||||
|
// t.Fatalf("[%d] %v: bad state. got:\n%+v\n\nwant:\n%+v\n",
|
||||||
|
// i, st.command, newState, st.want)
|
||||||
}
|
}
|
||||||
if newState != nil {
|
if newState != nil {
|
||||||
current = newState
|
current = newState
|
||||||
@ -121,6 +662,15 @@ func exactErr(want error, optName ...string) func(error) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmd(s string) []string {
|
// anyErr returns an error checker that wants any error.
|
||||||
return strings.Fields(s)
|
func anyErr() func(error) string {
|
||||||
|
return func(got error) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmd(s string) []string {
|
||||||
|
cmds := strings.Fields(s)
|
||||||
|
fmt.Printf("cmd: %v", cmds)
|
||||||
|
return cmds
|
||||||
}
|
}
|
||||||
|
@ -76,10 +76,10 @@ func (src *ServeConfig) Clone() *ServeConfig {
|
|||||||
dst.Web[k] = v.Clone()
|
dst.Web[k] = v.Clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dst.AllowIngress != nil {
|
if dst.AllowFunnel != nil {
|
||||||
dst.AllowIngress = map[HostPort]bool{}
|
dst.AllowFunnel = map[HostPort]bool{}
|
||||||
for k, v := range src.AllowIngress {
|
for k, v := range src.AllowFunnel {
|
||||||
dst.AllowIngress[k] = v
|
dst.AllowFunnel[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dst
|
return dst
|
||||||
@ -87,9 +87,9 @@ func (src *ServeConfig) Clone() *ServeConfig {
|
|||||||
|
|
||||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
|
var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
|
||||||
TCP map[uint16]*TCPPortHandler
|
TCP map[uint16]*TCPPortHandler
|
||||||
Web map[HostPort]*WebServerConfig
|
Web map[HostPort]*WebServerConfig
|
||||||
AllowIngress map[HostPort]bool
|
AllowFunnel map[HostPort]bool
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// Clone makes a deep copy of TCPPortHandler.
|
// Clone makes a deep copy of TCPPortHandler.
|
||||||
|
@ -176,15 +176,15 @@ func (v ServeConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServer
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v ServeConfigView) AllowIngress() views.Map[HostPort, bool] {
|
func (v ServeConfigView) AllowFunnel() views.Map[HostPort, bool] {
|
||||||
return views.MapOf(v.ж.AllowIngress)
|
return views.MapOf(v.ж.AllowFunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
|
var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
|
||||||
TCP map[uint16]*TCPPortHandler
|
TCP map[uint16]*TCPPortHandler
|
||||||
Web map[HostPort]*WebServerConfig
|
Web map[HostPort]*WebServerConfig
|
||||||
AllowIngress map[HostPort]bool
|
AllowFunnel map[HostPort]bool
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// View returns a readonly view of TCPPortHandler.
|
// View returns a readonly view of TCPPortHandler.
|
||||||
|
@ -2236,13 +2236,13 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
|||||||
// optimization hint to know primarily which nodes are NOT using ingress, to
|
// optimization hint to know primarily which nodes are NOT using ingress, to
|
||||||
// avoid doing work for regular nodes.
|
// avoid doing work for regular nodes.
|
||||||
//
|
//
|
||||||
// Even if the user's ServeConfig.AllowIngress map was manually edited in raw
|
// Even if the user's ServeConfig.AllowFunnel map was manually edited in raw
|
||||||
// mode and contains map entries with false values, sending true (from Len > 0)
|
// mode and contains map entries with false values, sending true (from Len > 0)
|
||||||
// is still fine. This is only an optimization hint for the control plane and
|
// is still fine. This is only an optimization hint for the control plane and
|
||||||
// doesn't affect security or correctness. And we also don't expect people to
|
// doesn't affect security or correctness. And we also don't expect people to
|
||||||
// modify their ServeConfig in raw mode.
|
// modify their ServeConfig in raw mode.
|
||||||
func (b *LocalBackend) wantIngressLocked() bool {
|
func (b *LocalBackend) wantIngressLocked() bool {
|
||||||
return b.serveConfig.Valid() && b.serveConfig.AllowIngress().Len() > 0
|
return b.serveConfig.Valid() && b.serveConfig.AllowFunnel().Len() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
||||||
|
@ -234,7 +234,7 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ip
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sc.AllowIngress().Get(target) {
|
if !sc.AllowFunnel().Get(target) {
|
||||||
b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target)
|
b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target)
|
||||||
sendRST()
|
sendRST()
|
||||||
return
|
return
|
||||||
|
@ -108,9 +108,9 @@ type ServeConfig struct {
|
|||||||
// keyed by mount point ("/", "/foo", etc)
|
// keyed by mount point ("/", "/foo", etc)
|
||||||
Web map[HostPort]*WebServerConfig `json:",omitempty"`
|
Web map[HostPort]*WebServerConfig `json:",omitempty"`
|
||||||
|
|
||||||
// AllowIngress is the set of SNI:port values for which ingress
|
// AllowFunnel is the set of SNI:port values for which funnel
|
||||||
// traffic is allowed, from trusted ingress peers.
|
// traffic is allowed, from trusted ingress peers.
|
||||||
AllowIngress map[HostPort]bool `json:",omitempty"`
|
AllowFunnel map[HostPort]bool `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostPort is an SNI name and port number, joined by a colon.
|
// HostPort is an SNI name and port number, joined by a colon.
|
||||||
@ -119,7 +119,7 @@ type HostPort string
|
|||||||
|
|
||||||
// WebServerConfig describes a web server's configuration.
|
// WebServerConfig describes a web server's configuration.
|
||||||
type WebServerConfig struct {
|
type WebServerConfig struct {
|
||||||
Handlers map[string]*HTTPHandler
|
Handlers map[string]*HTTPHandler // mountPoint => handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// TCPPortHandler describes what to do when handling a TCP
|
// TCPPortHandler describes what to do when handling a TCP
|
||||||
|
@ -1682,6 +1682,11 @@ const (
|
|||||||
CapabilityIngress = "https://tailscale.com/cap/ingress"
|
CapabilityIngress = "https://tailscale.com/cap/ingress"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NodeAttrFunnel grants the ability for a node to host ingress traffic.
|
||||||
|
NodeAttrFunnel = "funnel"
|
||||||
|
)
|
||||||
|
|
||||||
// SetDNSRequest is a request to add a DNS record.
|
// SetDNSRequest is a request to add a DNS record.
|
||||||
//
|
//
|
||||||
// This is used for ACME DNS-01 challenges (so people can use
|
// This is used for ACME DNS-01 challenges (so people can use
|
||||||
|
Loading…
x
Reference in New Issue
Block a user