c71e8db058
After:
bradfitz@book1pro tailscale.com % ./tool/go test -c ./cmd/tailscale/cli
bradfitz@book1pro tailscale.com % ./cli.test
bradfitz@book1pro tailscale.com %
Before:
bradfitz@book1pro tailscale.com % ./tool/go test -c ./cmd/tailscale/cli
bradfitz@book1pro tailscale.com % ./cli.test
Warning: funnel=on for foo.test.ts.net:443, but no serve config
run: `tailscale serve --help` to see how to configure handlers
Warning: funnel=on for foo.test.ts.net:443, but no serve config
run: `tailscale serve --help` to see how to configure handlers
USAGE
funnel <serve-port> {on|off}
funnel status [--json]
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.
SUBCOMMANDS
status show current serve/funnel status
error: path must be absolute
error: invalid TCP source "localhost:5432": missing port in address
error: invalid TCP source "tcp://somehost:5432"
must be one of: localhost or 127.0.0.1
tcp://somehost:5432error: invalid TCP source "tcp://somehost:0"
must be one of: localhost or 127.0.0.1
tcp://somehost:0error: invalid TCP source "tcp://somehost:65536"
must be one of: localhost or 127.0.0.1
tcp://somehost:65536error: path must be absolute
error: cannot serve web; already serving TCP
You don't have permission to enable this feature.
This also moves the color handling up to a generic spot so it's
not just one subcommand doing it itself. See
https://github.com/tailscale/tailscale/issues/11626#issuecomment-2041795129
Fixes #11643
Updates #11626
Change-Id: I3a49e659dcbce491f4a2cb784be20bab53f72303
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
178 lines
5.3 KiB
Go
178 lines
5.3 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
var funnelCmd = func() *ffcli.Command {
|
|
se := &serveEnv{lc: &localClient}
|
|
// previously used to serve legacy newFunnelCommand unless useWIPCode is true
|
|
// change is limited to make a revert easier and full cleanup to come after the relase.
|
|
// TODO(tylersmalley): cleanup and removal of newFunnelCommand as of 2023-10-16
|
|
return newServeV2Command(se, funnel)
|
|
}
|
|
|
|
// 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: "Turn on/off Funnel service",
|
|
ShortUsage: strings.Join([]string{
|
|
"funnel <serve-port> {on|off}",
|
|
"funnel status [--json]",
|
|
}, "\n "),
|
|
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)
|
|
}
|
|
|
|
port64, err := strconv.ParseUint(args[0], 10, 16)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
port := uint16(port64)
|
|
|
|
if on {
|
|
// Don't block from turning off existing Funnel if
|
|
// network configuration/capabilities have changed.
|
|
// Only block from starting new Funnels.
|
|
if err := e.verifyFunnelEnabled(ctx, port); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("getting client status: %w", 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
|
|
}
|
|
sc.SetFunnel(dnsName, port, on)
|
|
|
|
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
|
return err
|
|
}
|
|
printFunnelWarning(sc)
|
|
return nil
|
|
}
|
|
|
|
// verifyFunnelEnabled verifies that the self node is allowed to use Funnel.
|
|
//
|
|
// If Funnel is not yet enabled by the current node capabilities,
|
|
// the user is sent through an interactive flow to enable the feature.
|
|
// Once enabled, verifyFunnelEnabled checks that the given port is allowed
|
|
// with Funnel.
|
|
//
|
|
// If an error is reported, the CLI should stop execution and return the error.
|
|
//
|
|
// verifyFunnelEnabled may refresh the local state and modify the st input.
|
|
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, port uint16) error {
|
|
enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel)
|
|
st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block
|
|
switch {
|
|
case statusErr != nil:
|
|
return fmt.Errorf("getting client status: %w", statusErr)
|
|
case enableErr != nil:
|
|
// enableFeatureInteractive is a new flow behind a control server
|
|
// feature flag. If anything caused it to error, fallback to using
|
|
// the old CheckFunnelAccess call. Likely this domain does not have
|
|
// the feature flag on.
|
|
// TODO(sonia,tailscale/corp#10577): Remove this fallback once the
|
|
// control flag is turned on for all domains.
|
|
if err := ipn.CheckFunnelAccess(port, st.Self); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
// Done with enablement, make sure the requested port is allowed.
|
|
if err := ipn.CheckFunnelPort(port, st.Self); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
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(Stderr, "\nWarning: funnel=on for %s, but no serve config\n", hp)
|
|
}
|
|
}
|
|
if warn {
|
|
fmt.Fprintf(Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
|
|
}
|
|
}
|