mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
ipn: add Funnel port check from nodeAttr
Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
parent
ccdd534e81
commit
3ff44b2307
@ -189,15 +189,11 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -677,7 +673,7 @@ func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
if err := ipn.CheckFunnelAccess(st.Self.Capabilities); err != nil {
|
||||
if err := ipn.CheckFunnelAccess(srvPort, st.Self.Capabilities); err != nil {
|
||||
return err
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
|
@ -119,10 +119,6 @@ type step struct {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // invalid port
|
||||
command: cmd("--serve-port=9999 /abc proxy 3001"),
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--serve-port=8443 /abc proxy 3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
@ -653,7 +649,7 @@ type fakeLocalServeClient struct {
|
||||
BackendState: ipn.Running.String(),
|
||||
Self: &ipnstate.PeerStatus{
|
||||
DNSName: "foo.test.ts.net",
|
||||
Capabilities: []string{tailcfg.NodeAttrFunnel},
|
||||
Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
|
||||
},
|
||||
}
|
||||
|
||||
|
77
ipn/serve.go
77
ipn/serve.go
@ -5,8 +5,12 @@
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/tailcfg"
|
||||
@ -173,13 +177,18 @@ func (sc *ServeConfig) IsFunnelOn() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckFunnelAccess checks three things: 1) an invite was used to join the
|
||||
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
|
||||
// If any of these are false, an error is returned describing the problem.
|
||||
// CheckFunnelAccess checks whether Funnel access is allowed for the given node
|
||||
// and port.
|
||||
// It checks:
|
||||
// 1. an invite was used to join the Funnel alpha
|
||||
// 2. HTTPS is enabled on the Tailnet
|
||||
// 3. the node has the "funnel" nodeAttr
|
||||
// 4. the port is allowed for Funnel
|
||||
//
|
||||
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
|
||||
// the attribute we're checking for and possibly warning-capabilities for Funnel.
|
||||
func CheckFunnelAccess(nodeAttrs []string) error {
|
||||
// the attribute we're checking for and possibly warning-capabilities for
|
||||
// Funnel.
|
||||
func CheckFunnelAccess(port uint16, nodeAttrs []string) error {
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
|
||||
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
@ -189,5 +198,61 @@ func CheckFunnelAccess(nodeAttrs []string) error {
|
||||
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
|
||||
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
return nil
|
||||
return checkFunnelPort(port, nodeAttrs)
|
||||
}
|
||||
|
||||
// checkFunnelPort checks whether the given port is allowed for Funnel.
|
||||
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
|
||||
// ports.
|
||||
func checkFunnelPort(wantedPort uint16, nodeAttrs []string) error {
|
||||
deny := func(allowedPorts string) error {
|
||||
if allowedPorts == "" {
|
||||
return fmt.Errorf("port %d is not allowed for funnel", wantedPort)
|
||||
}
|
||||
return fmt.Errorf("port %d is not allowed for funnel; allowed ports are: %v", wantedPort, allowedPorts)
|
||||
}
|
||||
var portsStr string
|
||||
for _, attr := range nodeAttrs {
|
||||
if !strings.HasPrefix(attr, tailcfg.CapabilityFunnelPorts) {
|
||||
continue
|
||||
}
|
||||
u, err := url.Parse(attr)
|
||||
if err != nil {
|
||||
return deny("")
|
||||
}
|
||||
portsStr = u.Query().Get("ports")
|
||||
if portsStr == "" {
|
||||
return deny("")
|
||||
}
|
||||
u.RawQuery = ""
|
||||
if u.String() != tailcfg.CapabilityFunnelPorts {
|
||||
return deny("")
|
||||
}
|
||||
}
|
||||
wantedPortString := strconv.Itoa(int(wantedPort))
|
||||
for _, ps := range strings.Split(portsStr, ",") {
|
||||
if ps == "" {
|
||||
continue
|
||||
}
|
||||
first, last, ok := strings.Cut(ps, "-")
|
||||
if !ok {
|
||||
if first == wantedPortString {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
fp, err := strconv.ParseUint(first, 10, 16)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lp, err := strconv.ParseUint(last, 10, 16)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pr := tailcfg.PortRange{First: uint16(fp), Last: uint16(lp)}
|
||||
if pr.Contains(wantedPort) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return deny(portsStr)
|
||||
}
|
||||
|
@ -9,17 +9,24 @@
|
||||
)
|
||||
|
||||
func TestCheckFunnelAccess(t *testing.T) {
|
||||
portAttr := "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443,"
|
||||
tests := []struct {
|
||||
port uint16
|
||||
caps []string
|
||||
wantErr bool
|
||||
}{
|
||||
{[]string{}, true}, // No "funnel" attribute
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
|
||||
{[]string{tailcfg.NodeAttrFunnel}, false},
|
||||
{443, []string{portAttr}, true}, // No "funnel" attribute
|
||||
{443, []string{portAttr, tailcfg.CapabilityWarnFunnelNoInvite}, true},
|
||||
{443, []string{portAttr, tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
|
||||
{443, []string{portAttr, tailcfg.NodeAttrFunnel}, false},
|
||||
{8443, []string{portAttr, tailcfg.NodeAttrFunnel}, false},
|
||||
{8321, []string{portAttr, tailcfg.NodeAttrFunnel}, true},
|
||||
{8083, []string{portAttr, tailcfg.NodeAttrFunnel}, false},
|
||||
{8091, []string{portAttr, tailcfg.NodeAttrFunnel}, true},
|
||||
{3000, []string{portAttr, tailcfg.NodeAttrFunnel}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := CheckFunnelAccess(tt.caps)
|
||||
err := CheckFunnelAccess(tt.port, tt.caps)
|
||||
switch {
|
||||
case err != nil && tt.wantErr,
|
||||
err == nil && !tt.wantErr:
|
||||
|
@ -1058,6 +1058,11 @@ type PortRange struct {
|
||||
Last uint16
|
||||
}
|
||||
|
||||
// Contains reports whether port is in pr.
|
||||
func (pr PortRange) Contains(port uint16) bool {
|
||||
return port >= pr.First && port <= pr.Last
|
||||
}
|
||||
|
||||
var PortRangeAny = PortRange{0, 65535}
|
||||
|
||||
// NetPortRange represents a range of ports that's allowed for one or more IPs.
|
||||
@ -1818,6 +1823,12 @@ type Oauth2Token struct {
|
||||
// resolution for Tailscale-controlled domains (the control server, log
|
||||
// server, DERP servers, etc.)
|
||||
CapabilityDebugTSDNSResolution = "https://tailscale.com/cap/debug-ts-dns-resolution"
|
||||
|
||||
// CapabilityFunnelPorts specifies the ports that the Funnel is available on.
|
||||
// The ports are specified as a comma-separated list of port numbers or port
|
||||
// ranges (e.g. "80,443,8080-8090") in the ports query parameter.
|
||||
// e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090
|
||||
CapabilityFunnelPorts = "https://tailscale.com/cap/funnel-ports"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -22,7 +22,7 @@
|
||||
func main() {
|
||||
flag.Parse()
|
||||
s := &tsnet.Server{
|
||||
Dir: "./funnel-demo-config.state",
|
||||
Dir: "./funnel-demo-config",
|
||||
Logf: logger.Discard,
|
||||
Hostname: "fun",
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -767,7 +768,7 @@ func (s *Server) Listen(network, addr string) (net.Listener, error) {
|
||||
// ListenTLS announces only on the Tailscale network.
|
||||
// It returns a TLS listener wrapping the tsnet listener.
|
||||
// It will start the server if it has not been started yet.
|
||||
func (s *Server) ListenTLS(network string, addr string) (net.Listener, error) {
|
||||
func (s *Server) ListenTLS(network, addr string) (net.Listener, error) {
|
||||
if network != "tcp" {
|
||||
return nil, fmt.Errorf("ListenTLS(%q, %q): only tcp is supported", network, addr)
|
||||
}
|
||||
@ -822,28 +823,32 @@ func FunnelOnly() FunnelOption { return funnelOnly(1) }
|
||||
// and the only other supported addrs currently are ":8443" and ":10000".
|
||||
//
|
||||
// It will start the server if it has not been started yet.
|
||||
func (s *Server) ListenFunnel(network string, addr string, opts ...FunnelOption) (net.Listener, error) {
|
||||
func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.Listener, error) {
|
||||
if network != "tcp" {
|
||||
return nil, fmt.Errorf("ListenFunnel(%q, %q): only tcp is supported", network, addr)
|
||||
}
|
||||
switch addr {
|
||||
case ":443", ":8443", ":10000":
|
||||
default:
|
||||
return nil, fmt.Errorf(`ListenFunnel(%q, %q): only valid addrs are ":443", ":8443", and ":10000"`, network, addr)
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if host != "" {
|
||||
return nil, fmt.Errorf("ListenFunnel(%q, %q): host must be empty", network, addr)
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
st, err := s.Up(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(st.CertDomains) == 0 {
|
||||
return nil, errors.New("tsnet: you must enable HTTPS in the admin panel to proceed")
|
||||
}
|
||||
if err := ipn.CheckFunnelAccess(st.Self.Capabilities); err != nil {
|
||||
if err := ipn.CheckFunnelAccess(uint16(port), st.Self.Capabilities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lc, err := s.LocalClient() // do local client first before listening.
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -857,7 +862,7 @@ func (s *Server) ListenFunnel(network string, addr string, opts ...FunnelOption)
|
||||
srvConfig = &ipn.ServeConfig{}
|
||||
}
|
||||
domain := st.CertDomains[0]
|
||||
hp := ipn.HostPort(domain + addr) // valid only because of the strong restrictions on addr above
|
||||
hp := ipn.HostPort(domain + ":" + portStr)
|
||||
if !srvConfig.AllowFunnel[hp] {
|
||||
mak.Set(&srvConfig.AllowFunnel, hp, true)
|
||||
srvConfig.AllowFunnel[hp] = true
|
||||
|
Loading…
Reference in New Issue
Block a user