ipn: use NodeCapMap in CheckFunnel

These were missed when adding NodeCapMap and resulted
in tsnet binaries not being able to turn on funnel.

Fixes #9566

Signed-off-by: Maisem Ali <maisem@tailscale.com>
pull/9572/head
Maisem Ali 1 year ago committed by Maisem Ali
parent 5c2b2fa1f8
commit 354455e8be

@ -270,7 +270,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
runtime/metrics from github.com/prometheus/client_golang/prometheus+ runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof runtime/trace from net/http/pprof
slices from tailscale.com/ipn+ slices from tailscale.com/ipn/ipnstate+
sort from compress/flate+ sort from compress/flate+
strconv from compress/flate+ strconv from compress/flate+
strings from bufio+ strings from bufio+

@ -164,12 +164,12 @@ func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status,
// the feature flag on. // the feature flag on.
// TODO(sonia,tailscale/corp#10577): Remove this fallback once the // TODO(sonia,tailscale/corp#10577): Remove this fallback once the
// control flag is turned on for all domains. // control flag is turned on for all domains.
if err := ipn.CheckFunnelAccess(port, st.Self.Capabilities); err != nil { if err := ipn.CheckFunnelAccess(port, st.Self); err != nil {
return err return err
} }
default: default:
// Done with enablement, make sure the requested port is allowed. // Done with enablement, make sure the requested port is allowed.
if err := ipn.CheckFunnelPort(port, st.Self.Capabilities); err != nil { if err := ipn.CheckFunnelPort(port, st.Self); err != nil {
return err return err
} }
} }

@ -9,10 +9,10 @@ import (
"net" "net"
"net/netip" "net/netip"
"net/url" "net/url"
"slices"
"strconv" "strconv"
"strings" "strings"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@ -237,23 +237,21 @@ func (sc *ServeConfig) IsFunnelOn() bool {
// 2. the node has the "funnel" nodeAttr // 2. the node has the "funnel" nodeAttr
// 3. the port is allowed for Funnel // 3. the port is allowed for Funnel
// //
// The nodeAttrs arg should be the node's Self.Capabilities which should contain // The node arg should be the ipnstate.Status.Self node.
// the attribute we're checking for and possibly warning-capabilities for func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error {
// Funnel. if !node.HasCap(tailcfg.CapabilityHTTPS) {
func CheckFunnelAccess(port uint16, nodeAttrs []tailcfg.NodeCapability) error {
if !slices.Contains(nodeAttrs, tailcfg.CapabilityHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.") return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
} }
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) { if !node.HasCap(tailcfg.NodeAttrFunnel) {
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.") return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.")
} }
return CheckFunnelPort(port, nodeAttrs) return CheckFunnelPort(port, node)
} }
// CheckFunnelPort checks whether the given port is allowed for Funnel. // CheckFunnelPort checks whether the given port is allowed for Funnel.
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed // It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
// ports. // ports.
func CheckFunnelPort(wantedPort uint16, nodeAttrs []tailcfg.NodeCapability) error { func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error {
deny := func(allowedPorts string) error { deny := func(allowedPorts string) error {
if allowedPorts == "" { if allowedPorts == "" {
return fmt.Errorf("port %d is not allowed for funnel", wantedPort) return fmt.Errorf("port %d is not allowed for funnel", wantedPort)
@ -261,24 +259,50 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []tailcfg.NodeCapability) erro
return fmt.Errorf("port %d is not allowed for funnel; allowed ports are: %v", wantedPort, allowedPorts) return fmt.Errorf("port %d is not allowed for funnel; allowed ports are: %v", wantedPort, allowedPorts)
} }
var portsStr string var portsStr string
for _, attr := range nodeAttrs { parseAttr := func(attr string) (string, error) {
u, err := url.Parse(attr)
if err != nil {
return "", deny("")
}
portsStr := u.Query().Get("ports")
if portsStr == "" {
return "", deny("")
}
u.RawQuery = ""
if u.String() != string(tailcfg.CapabilityFunnelPorts) {
return "", deny("")
}
return portsStr, nil
}
for attr := range node.CapMap {
attr := string(attr) attr := string(attr)
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) { if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
continue continue
} }
u, err := url.Parse(attr) var err error
portsStr, err = parseAttr(attr)
if err != nil { if err != nil {
return deny("") return err
}
break
} }
portsStr = u.Query().Get("ports")
if portsStr == "" { if portsStr == "" {
return deny("") for _, attr := range node.Capabilities {
attr := string(attr)
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
continue
} }
u.RawQuery = "" var err error
if u.String() != string(tailcfg.CapabilityFunnelPorts) { portsStr, err = parseAttr(attr)
return deny("") if err != nil {
return err
}
break
} }
} }
if portsStr == "" {
return deny("")
}
wantedPortString := strconv.Itoa(int(wantedPort)) wantedPortString := strconv.Itoa(int(wantedPort))
for _, ps := range strings.Split(portsStr, ",") { for _, ps := range strings.Split(portsStr, ",") {
if ps == "" { if ps == "" {

@ -5,6 +5,7 @@ package ipn
import ( import (
"testing" "testing"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@ -26,7 +27,11 @@ func TestCheckFunnelAccess(t *testing.T) {
{3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true}, {3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
} }
for _, tt := range tests { for _, tt := range tests {
err := CheckFunnelAccess(tt.port, tt.caps) cm := tailcfg.NodeCapMap{}
for _, c := range tt.caps {
cm[c] = nil
}
err := CheckFunnelAccess(tt.port, &ipnstate.PeerStatus{CapMap: cm})
switch { switch {
case err != nil && tt.wantErr, case err != nil && tt.wantErr,
err == nil && !tt.wantErr: err == nil && !tt.wantErr:

@ -926,7 +926,7 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
// flow here instead of CheckFunnelAccess to allow the user to turn on Funnel // flow here instead of CheckFunnelAccess to allow the user to turn on Funnel
// if not already on. Specifically when running from a terminal. // if not already on. Specifically when running from a terminal.
// See cli.serveEnv.verifyFunnelEnabled. // See cli.serveEnv.verifyFunnelEnabled.
if err := ipn.CheckFunnelAccess(uint16(port), st.Self.Capabilities); err != nil { if err := ipn.CheckFunnelAccess(uint16(port), st.Self); err != nil {
return nil, err return nil, err
} }

Loading…
Cancel
Save