177 lines
5.3 KiB
Go
Raw Normal View History

// 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{
"tailscale funnel <serve-port> {on|off}",
"tailscale 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,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortUsage: "tailscale funnel status [--json]",
ShortHelp: "Show current serve/funnel status",
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
},
},
}
}
// 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
cmd/tailscale/cli: stop spamming os.Stdout/os.Stderr in tests 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>
2024-04-07 18:17:25 -07:00
fmt.Fprintf(Stderr, "\nWarning: funnel=on for %s, but no serve config\n", hp)
}
}
if warn {
cmd/tailscale/cli: stop spamming os.Stdout/os.Stderr in tests 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>
2024-04-07 18:17:25 -07:00
fmt.Fprintf(Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
}
}