cmd/tailscale/cli: use status before doing interactive feature query

We were inconsistent whether we checked if the feature was already
enabled which we could do cheaply using the locally available status.
We would do the checks fine if we were turning on funnel, but not serve.

This moves the cap checks down into enableFeatureInteractive so that
are always run.

Updates #9984

Co-authored-by: Tyler Smalley <tyler@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
bradfitz/tbug
Maisem Ali 1 year ago committed by Maisem Ali
parent e94d345e26
commit 9107b5eadf

@ -14,7 +14,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/mak" "tailscale.com/util/mak"
) )
@ -88,10 +87,6 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
if sc == nil { if sc == nil {
sc = new(ipn.ServeConfig) sc = new(ipn.ServeConfig)
} }
st, err := e.getLocalClientStatusWithoutPeers(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
port64, err := strconv.ParseUint(args[0], 10, 16) port64, err := strconv.ParseUint(args[0], 10, 16)
if err != nil { if err != nil {
@ -103,11 +98,15 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
// Don't block from turning off existing Funnel if // Don't block from turning off existing Funnel if
// network configuration/capabilities have changed. // network configuration/capabilities have changed.
// Only block from starting new Funnels. // Only block from starting new Funnels.
if err := e.verifyFunnelEnabled(ctx, st, port); err != nil { if err := e.verifyFunnelEnabled(ctx, port); err != nil {
return err return err
} }
} }
st, err := e.getLocalClientStatusWithoutPeers(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
dnsName := strings.TrimSuffix(st.Self.DNSName, ".") dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port))) hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port)))
if on == sc.AllowFunnel[hp] { if on == sc.AllowFunnel[hp] {
@ -141,13 +140,7 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
// If an error is reported, the CLI should stop execution and return the error. // 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. // verifyFunnelEnabled may refresh the local state and modify the st input.
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status, port uint16) error { func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, port uint16) error {
hasFunnelAttrs := func(selfNode *ipnstate.PeerStatus) bool {
return selfNode.HasCap(tailcfg.CapabilityHTTPS) && selfNode.HasCap(tailcfg.NodeAttrFunnel)
}
if hasFunnelAttrs(st.Self) {
return nil // already enabled
}
enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel) enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel)
st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block
switch { switch {

@ -818,6 +818,24 @@ func parseServePort(s string) (uint16, error) {
// 2023-08-09: The only valid feature values are "serve" and "funnel". // 2023-08-09: The only valid feature values are "serve" and "funnel".
// This can be moved to some CLI lib when expanded past serve/funnel. // This can be moved to some CLI lib when expanded past serve/funnel.
func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) { func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) {
st, err := e.getLocalClientStatusWithoutPeers(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
if st.Self == nil {
return errors.New("no self node")
}
hasCaps := func() bool {
for _, c := range caps {
if !st.Self.HasCap(c) {
return false
}
}
return true
}
if hasCaps() {
return nil // already enabled
}
info, err := e.lc.QueryFeature(ctx, feature) info, err := e.lc.QueryFeature(ctx, feature)
if err != nil { if err != nil {
return err return err

@ -786,7 +786,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
{ {
name: "fallback-flow-enabled", name: "fallback-flow-enabled",
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")}, queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel, "https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090"},
wantErr: "", // no error, success wantErr: "", // no error, success
}, },
{ {
@ -811,10 +811,6 @@ func TestVerifyFunnelEnabled(t *testing.T) {
defer func() { fakeStatus.Self.Capabilities = oldCaps }() // reset after test defer func() { fakeStatus.Self.Capabilities = oldCaps }() // reset after test
fakeStatus.Self.Capabilities = tt.caps fakeStatus.Self.Capabilities = tt.caps
} }
st, err := e.getLocalClientStatusWithoutPeers(ctx)
if err != nil {
t.Fatal(err)
}
defer func() { defer func() {
r := recover() r := recover()
@ -826,7 +822,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
t.Errorf("wrong panic; got=%s, want=%s", gotPanic, tt.wantPanic) t.Errorf("wrong panic; got=%s, want=%s", gotPanic, tt.wantPanic)
} }
}() }()
gotErr := e.verifyFunnelEnabled(ctx, st, 443) gotErr := e.verifyFunnelEnabled(ctx, 443)
var got string var got string
if gotErr != nil { if gotErr != nil {
got = gotErr.Error() got = gotErr.Error()

@ -213,15 +213,10 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel() defer cancel()
st, err := e.getLocalClientStatusWithoutPeers(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
funnel := subcmd == funnel funnel := subcmd == funnel
if funnel { if funnel {
// verify node has funnel capabilities // verify node has funnel capabilities
if err := e.verifyFunnelEnabled(ctx, st, 443); err != nil { if err := e.verifyFunnelEnabled(ctx, 443); err != nil {
return err return err
} }
} }
@ -246,6 +241,10 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
if sc == nil { if sc == nil {
sc = new(ipn.ServeConfig) sc = new(ipn.ServeConfig)
} }
st, err := e.getLocalClientStatusWithoutPeers(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
dnsName := strings.TrimSuffix(st.Self.DNSName, ".") dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
// set parent serve config to always be persisted // set parent serve config to always be persisted

Loading…
Cancel
Save