mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 21:27:31 +00:00
{ipn/serve,cmd/tailscale/cli}: move some shared funcs to ipn
In preparation for changes to allow configuration of serve/funnel from the web client, this commit moves some functionality that will be shared between the CLI and web client to the ipn package's serve.go file, where some other util funcs are already defined. Updates #10261 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:

committed by
Sonia Appasamy

parent
8780e33500
commit
65c3c690cf
86
ipn/serve.go
86
ipn/serve.go
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -234,6 +235,24 @@ func (sc *ServeConfig) IsServingHTTP(port uint16) bool {
|
||||
return sc.TCP[port].HTTP
|
||||
}
|
||||
|
||||
// FindConfig finds a config that contains the given port, which can be
|
||||
// the top level background config or an inner foreground one.
|
||||
// The second result is true if it's foreground.
|
||||
func (sc *ServeConfig) FindConfig(port uint16) (*ServeConfig, bool) {
|
||||
if sc == nil {
|
||||
return nil, false
|
||||
}
|
||||
if _, ok := sc.TCP[port]; ok {
|
||||
return sc, false
|
||||
}
|
||||
for _, sc := range sc.Foreground {
|
||||
if _, ok := sc.TCP[port]; ok {
|
||||
return sc, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
|
||||
// traffic for any host:port.
|
||||
//
|
||||
@@ -257,19 +276,28 @@ func (sc *ServeConfig) IsFunnelOn() bool {
|
||||
// CheckFunnelAccess checks whether Funnel access is allowed for the given node
|
||||
// and port.
|
||||
// It checks:
|
||||
// 1. HTTPS is enabled on the Tailnet
|
||||
// 1. HTTPS is enabled on the tailnet
|
||||
// 2. the node has the "funnel" nodeAttr
|
||||
// 3. the port is allowed for Funnel
|
||||
//
|
||||
// The node arg should be the ipnstate.Status.Self node.
|
||||
func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error {
|
||||
if err := NodeCanFunnel(node); err != nil {
|
||||
return err
|
||||
}
|
||||
return CheckFunnelPort(port, node)
|
||||
}
|
||||
|
||||
// NodeCanFunnel returns an error if the given node is not configured to allow
|
||||
// for Tailscale Funnel usage.
|
||||
func NodeCanFunnel(node *ipnstate.PeerStatus) error {
|
||||
if !node.HasCap(tailcfg.CapabilityHTTPS) {
|
||||
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
|
||||
}
|
||||
if !node.HasCap(tailcfg.NodeAttrFunnel) {
|
||||
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.")
|
||||
}
|
||||
return CheckFunnelPort(port, node)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckFunnelPort checks whether the given port is allowed for Funnel.
|
||||
@@ -355,6 +383,60 @@ func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error {
|
||||
return deny(portsStr)
|
||||
}
|
||||
|
||||
// ExpandProxyTargetValue expands the supported target values to be proxied
|
||||
// allowing for input values to be a port number, a partial URL, or a full URL
|
||||
// including a path.
|
||||
//
|
||||
// examples:
|
||||
// - 3000
|
||||
// - localhost:3000
|
||||
// - tcp://localhost:3000
|
||||
// - http://localhost:3000
|
||||
// - https://localhost:3000
|
||||
// - https-insecure://localhost:3000
|
||||
// - https-insecure://localhost:3000/foo
|
||||
func ExpandProxyTargetValue(target string, supportedSchemes []string, defaultScheme string) (string, error) {
|
||||
const host = "127.0.0.1"
|
||||
|
||||
// support target being a port number
|
||||
if port, err := strconv.ParseUint(target, 10, 16); err == nil {
|
||||
return fmt.Sprintf("%s://%s:%d", defaultScheme, host, port), nil
|
||||
}
|
||||
|
||||
// prepend scheme if not present
|
||||
if !strings.Contains(target, "://") {
|
||||
target = defaultScheme + "://" + target
|
||||
}
|
||||
|
||||
// make sure we can parse the target
|
||||
u, err := url.ParseRequestURI(target)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid URL %w", err)
|
||||
}
|
||||
|
||||
// ensure a supported scheme
|
||||
if !slices.Contains(supportedSchemes, u.Scheme) {
|
||||
return "", fmt.Errorf("must be a URL starting with one of the supported schemes: %v", supportedSchemes)
|
||||
}
|
||||
|
||||
// validate the host.
|
||||
switch u.Hostname() {
|
||||
case "localhost", "127.0.0.1":
|
||||
default:
|
||||
return "", errors.New("only localhost or 127.0.0.1 proxies are currently supported")
|
||||
}
|
||||
|
||||
// validate the port
|
||||
port, err := strconv.ParseUint(u.Port(), 10, 16)
|
||||
if err != nil || port == 0 {
|
||||
return "", fmt.Errorf("invalid port %q", u.Port())
|
||||
}
|
||||
|
||||
u.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// RangeOverTCPs ranges over both background and foreground TCPs.
|
||||
// If the returned bool from the given f is false, then this function stops
|
||||
// iterating immediately and does not check other foreground configs.
|
||||
|
@@ -126,3 +126,60 @@ func TestHasPathHandler(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandProxyTargetDev(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
defaultScheme string
|
||||
supportedSchemes []string
|
||||
expected string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "port-only", input: "8080", expected: "http://127.0.0.1:8080"},
|
||||
{name: "hostname+port", input: "localhost:8080", expected: "http://127.0.0.1:8080"},
|
||||
{name: "convert-localhost", input: "http://localhost:8080", expected: "http://127.0.0.1:8080"},
|
||||
{name: "no-change", input: "http://127.0.0.1:8080", expected: "http://127.0.0.1:8080"},
|
||||
{name: "include-path", input: "http://127.0.0.1:8080/foo", expected: "http://127.0.0.1:8080/foo"},
|
||||
{name: "https-scheme", input: "https://localhost:8080", expected: "https://127.0.0.1:8080"},
|
||||
{name: "https+insecure-scheme", input: "https+insecure://localhost:8080", expected: "https+insecure://127.0.0.1:8080"},
|
||||
{name: "change-default-scheme", input: "localhost:8080", defaultScheme: "https", expected: "https://127.0.0.1:8080"},
|
||||
{name: "change-supported-schemes", input: "localhost:8080", defaultScheme: "tcp", supportedSchemes: []string{"tcp"}, expected: "tcp://127.0.0.1:8080"},
|
||||
|
||||
// errors
|
||||
{name: "invalid-port", input: "localhost:9999999", wantErr: true},
|
||||
{name: "unsupported-scheme", input: "ftp://localhost:8080", expected: "", wantErr: true},
|
||||
{name: "not-localhost", input: "https://tailscale.com:8080", expected: "", wantErr: true},
|
||||
{name: "empty-input", input: "", expected: "", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
defaultScheme := "http"
|
||||
supportedSchemes := []string{"http", "https", "https+insecure"}
|
||||
|
||||
if tt.supportedSchemes != nil {
|
||||
supportedSchemes = tt.supportedSchemes
|
||||
}
|
||||
if tt.defaultScheme != "" {
|
||||
defaultScheme = tt.defaultScheme
|
||||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual, err := ExpandProxyTargetValue(tt.input, supportedSchemes, defaultScheme)
|
||||
|
||||
if tt.wantErr == true && err == nil {
|
||||
t.Errorf("Expected an error but got none")
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantErr == false && err != nil {
|
||||
t.Errorf("Got an error, but didn't expect one: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if actual != tt.expected {
|
||||
t.Errorf("Got: %q; expected: %q", actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user