2023-01-27 21:37:20 +00:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2022-11-19 14:42:14 +00:00
|
|
|
|
|
|
|
package ipn
|
|
|
|
|
2023-03-08 20:36:41 +00:00
|
|
|
import (
|
2023-03-10 19:44:28 +00:00
|
|
|
"errors"
|
2023-03-11 16:45:40 +00:00
|
|
|
"fmt"
|
2023-03-08 20:36:41 +00:00
|
|
|
"net"
|
|
|
|
"net/netip"
|
2023-03-11 16:45:40 +00:00
|
|
|
"net/url"
|
2024-03-05 18:54:37 +00:00
|
|
|
"slices"
|
2023-03-11 16:45:40 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2023-03-10 19:44:28 +00:00
|
|
|
|
2023-09-28 06:01:09 +00:00
|
|
|
"tailscale.com/ipn/ipnstate"
|
2023-03-10 19:44:28 +00:00
|
|
|
"tailscale.com/tailcfg"
|
2024-03-05 23:46:42 +00:00
|
|
|
"tailscale.com/util/mak"
|
2023-03-08 20:36:41 +00:00
|
|
|
)
|
|
|
|
|
2022-11-19 14:42:14 +00:00
|
|
|
// ServeConfigKey returns a StateKey that stores the
|
|
|
|
// JSON-encoded ServeConfig for a config profile.
|
|
|
|
func ServeConfigKey(profileID ProfileID) StateKey {
|
|
|
|
return StateKey("_serve/" + profileID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ServeConfig is the JSON type stored in the StateStore for
|
|
|
|
// StateKey "_serve/$PROFILE_ID" as returned by ServeConfigKey.
|
|
|
|
type ServeConfig struct {
|
|
|
|
// TCP are the list of TCP port numbers that tailscaled should handle for
|
|
|
|
// the Tailscale IP addresses. (not subnet routers, etc)
|
|
|
|
TCP map[uint16]*TCPPortHandler `json:",omitempty"`
|
|
|
|
|
|
|
|
// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers
|
|
|
|
// keyed by mount point ("/", "/foo", etc)
|
|
|
|
Web map[HostPort]*WebServerConfig `json:",omitempty"`
|
|
|
|
|
|
|
|
// AllowFunnel is the set of SNI:port values for which funnel
|
|
|
|
// traffic is allowed, from trusted ingress peers.
|
|
|
|
AllowFunnel map[HostPort]bool `json:",omitempty"`
|
2023-09-05 18:33:18 +00:00
|
|
|
|
2023-09-05 17:51:52 +00:00
|
|
|
// Foreground is a map of an IPN Bus session ID to an alternate foreground
|
|
|
|
// serve config that's valid for the life of that WatchIPNBus session ID.
|
|
|
|
// This. This allows the config to specify ephemeral configs that are
|
|
|
|
// used in the CLI's foreground mode to ensure ungraceful shutdowns
|
|
|
|
// of either the client or the LocalBackend does not expose ports
|
|
|
|
// that users are not aware of.
|
2023-09-05 18:33:18 +00:00
|
|
|
Foreground map[string]*ServeConfig `json:",omitempty"`
|
2023-09-12 01:32:51 +00:00
|
|
|
|
|
|
|
// ETag is the checksum of the serve config that's populated
|
|
|
|
// by the LocalClient through the HTTP ETag header during a
|
|
|
|
// GetServeConfig request and is translated to an If-Match header
|
|
|
|
// during a SetServeConfig request.
|
|
|
|
ETag string `json:"-"`
|
2022-11-19 14:42:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// HostPort is an SNI name and port number, joined by a colon.
|
|
|
|
// There is no implicit port 443. It must contain a colon.
|
|
|
|
type HostPort string
|
|
|
|
|
2023-08-17 15:47:35 +00:00
|
|
|
// Port extracts just the port number from hp.
|
|
|
|
// An error is reported in the case that the hp does not
|
|
|
|
// have a valid numeric port ending.
|
|
|
|
func (hp HostPort) Port() (uint16, error) {
|
|
|
|
_, port, err := net.SplitHostPort(string(hp))
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
port16, err := strconv.ParseUint(port, 10, 16)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return uint16(port16), nil
|
|
|
|
}
|
|
|
|
|
2023-03-08 20:36:41 +00:00
|
|
|
// A FunnelConn wraps a net.Conn that is coming over a
|
|
|
|
// Funnel connection. It can be used to determine further
|
|
|
|
// information about the connection, like the source address
|
|
|
|
// and the target SNI name.
|
|
|
|
type FunnelConn struct {
|
|
|
|
// Conn is the underlying connection.
|
|
|
|
net.Conn
|
|
|
|
|
|
|
|
// Target is what was presented in the "Tailscale-Ingress-Target"
|
|
|
|
// HTTP header.
|
|
|
|
Target HostPort
|
|
|
|
|
|
|
|
// Src is the source address of the connection.
|
|
|
|
// This is the address of the client that initiated the
|
|
|
|
// connection, not the address of the Tailscale Funnel
|
|
|
|
// node which is relaying the connection. That address
|
|
|
|
// can be found in Conn.RemoteAddr.
|
|
|
|
Src netip.AddrPort
|
|
|
|
}
|
|
|
|
|
2022-11-19 14:42:14 +00:00
|
|
|
// WebServerConfig describes a web server's configuration.
|
|
|
|
type WebServerConfig struct {
|
|
|
|
Handlers map[string]*HTTPHandler // mountPoint => handler
|
|
|
|
}
|
|
|
|
|
|
|
|
// TCPPortHandler describes what to do when handling a TCP
|
|
|
|
// connection.
|
|
|
|
type TCPPortHandler struct {
|
|
|
|
// HTTPS, if true, means that tailscaled should handle this connection as an
|
|
|
|
// HTTPS request as configured by ServeConfig.Web.
|
|
|
|
//
|
|
|
|
// It is mutually exclusive with TCPForward.
|
|
|
|
HTTPS bool `json:",omitempty"`
|
|
|
|
|
2023-06-21 16:32:20 +00:00
|
|
|
// HTTP, if true, means that tailscaled should handle this connection as an
|
|
|
|
// HTTP request as configured by ServeConfig.Web.
|
|
|
|
//
|
|
|
|
// It is mutually exclusive with TCPForward.
|
|
|
|
HTTP bool `json:",omitempty"`
|
|
|
|
|
2022-11-19 14:42:14 +00:00
|
|
|
// TCPForward is the IP:port to forward TCP connections to.
|
|
|
|
// Whether or not TLS is terminated by tailscaled depends on
|
|
|
|
// TerminateTLS.
|
|
|
|
//
|
|
|
|
// It is mutually exclusive with HTTPS.
|
|
|
|
TCPForward string `json:",omitempty"`
|
|
|
|
|
|
|
|
// TerminateTLS, if non-empty, means that tailscaled should terminate the
|
|
|
|
// TLS connections before forwarding them to TCPForward, permitting only the
|
|
|
|
// SNI name with this value. It is only used if TCPForward is non-empty.
|
|
|
|
// (the HTTPS mode uses ServeConfig.Web)
|
|
|
|
TerminateTLS string `json:",omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTTPHandler is either a path or a proxy to serve.
|
|
|
|
type HTTPHandler struct {
|
|
|
|
// Exactly one of the following may be set.
|
|
|
|
|
|
|
|
Path string `json:",omitempty"` // absolute path to directory or file to serve
|
|
|
|
Proxy string `json:",omitempty"` // http://localhost:3000/, localhost:3030, 3030
|
|
|
|
|
|
|
|
Text string `json:",omitempty"` // plaintext to serve (primarily for testing)
|
|
|
|
|
|
|
|
// TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
|
|
|
|
// temporary ones? Error codes? Redirects?
|
|
|
|
}
|
|
|
|
|
2023-06-21 16:32:20 +00:00
|
|
|
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
|
2022-11-19 14:42:14 +00:00
|
|
|
// the given host:port and mount point.
|
|
|
|
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
|
|
|
|
h := sc.GetWebHandler(hp, mount)
|
|
|
|
return h != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetWebHandler returns the HTTPHandler for the given host:port and mount point.
|
|
|
|
// Returns nil if the handler does not exist.
|
|
|
|
func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler {
|
2022-12-08 03:17:40 +00:00
|
|
|
if sc == nil || sc.Web[hp] == nil {
|
|
|
|
return nil
|
2022-11-19 14:42:14 +00:00
|
|
|
}
|
2022-12-08 03:17:40 +00:00
|
|
|
return sc.Web[hp].Handlers[mount]
|
2022-11-19 14:42:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetTCPPortHandler returns the TCPPortHandler for the given port.
|
|
|
|
// If the port is not configured, nil is returned.
|
|
|
|
func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
|
2022-12-08 03:17:40 +00:00
|
|
|
if sc == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2022-11-19 14:42:14 +00:00
|
|
|
return sc.TCP[port]
|
|
|
|
}
|
|
|
|
|
2023-10-26 21:40:44 +00:00
|
|
|
// HasPathHandler reports whether if ServeConfig has at least
|
|
|
|
// one path handler, including foreground configs.
|
|
|
|
func (sc *ServeConfig) HasPathHandler() bool {
|
|
|
|
if sc.Web != nil {
|
|
|
|
for _, webServerConfig := range sc.Web {
|
|
|
|
for _, httpHandler := range webServerConfig.Handlers {
|
|
|
|
if httpHandler.Path != "" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if sc.Foreground != nil {
|
|
|
|
for _, fgConfig := range sc.Foreground {
|
|
|
|
if fgConfig.HasPathHandler() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-06-21 16:32:20 +00:00
|
|
|
// IsTCPForwardingAny reports whether ServeConfig is currently forwarding in
|
|
|
|
// TCPForward mode on any port. This is exclusive of Web/HTTPS serving.
|
2022-11-19 14:42:14 +00:00
|
|
|
func (sc *ServeConfig) IsTCPForwardingAny() bool {
|
2022-12-08 03:17:40 +00:00
|
|
|
if sc == nil || len(sc.TCP) == 0 {
|
2022-11-19 14:42:14 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, h := range sc.TCP {
|
|
|
|
if h.TCPForward != "" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-06-21 16:32:20 +00:00
|
|
|
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding
|
|
|
|
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving.
|
2022-11-19 14:42:14 +00:00
|
|
|
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
|
2022-12-08 03:17:40 +00:00
|
|
|
if sc == nil || sc.TCP[port] == nil {
|
2022-11-19 14:42:14 +00:00
|
|
|
return false
|
|
|
|
}
|
2023-06-21 16:32:20 +00:00
|
|
|
return !sc.IsServingWeb(port)
|
2022-11-19 14:42:14 +00:00
|
|
|
}
|
|
|
|
|
2023-06-21 16:32:20 +00:00
|
|
|
// IsServingWeb reports whether if ServeConfig is currently serving Web
|
|
|
|
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding.
|
2022-11-19 14:42:14 +00:00
|
|
|
func (sc *ServeConfig) IsServingWeb(port uint16) bool {
|
2023-06-21 16:32:20 +00:00
|
|
|
return sc.IsServingHTTP(port) || sc.IsServingHTTPS(port)
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsServingHTTPS reports whether if ServeConfig is currently serving HTTPS on
|
|
|
|
// the given port. This is exclusive of HTTP and TCPForwarding.
|
|
|
|
func (sc *ServeConfig) IsServingHTTPS(port uint16) bool {
|
2022-12-08 03:17:40 +00:00
|
|
|
if sc == nil || sc.TCP[port] == nil {
|
2022-11-19 14:42:14 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
return sc.TCP[port].HTTPS
|
|
|
|
}
|
|
|
|
|
2023-06-21 16:32:20 +00:00
|
|
|
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the
|
|
|
|
// given port. This is exclusive of HTTPS and TCPForwarding.
|
|
|
|
func (sc *ServeConfig) IsServingHTTP(port uint16) bool {
|
|
|
|
if sc == nil || sc.TCP[port] == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return sc.TCP[port].HTTP
|
|
|
|
}
|
|
|
|
|
2024-03-05 18:54:37 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-03-05 23:46:42 +00:00
|
|
|
// SetWebHandler sets the given HTTPHandler at the specified host, port,
|
|
|
|
// and mount in the serve config. sc.TCP is also updated to reflect web
|
|
|
|
// serving usage of the given port.
|
|
|
|
func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uint16, mount string, useTLS bool) {
|
|
|
|
if sc == nil {
|
|
|
|
sc = new(ServeConfig)
|
|
|
|
}
|
|
|
|
mak.Set(&sc.TCP, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
|
|
|
|
|
|
|
|
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
|
|
|
|
if _, ok := sc.Web[hp]; !ok {
|
|
|
|
mak.Set(&sc.Web, hp, new(WebServerConfig))
|
|
|
|
}
|
|
|
|
mak.Set(&sc.Web[hp].Handlers, mount, handler)
|
|
|
|
|
|
|
|
// TODO(tylersmalley): handle multiple web handlers from foreground mode
|
|
|
|
for k, v := range sc.Web[hp].Handlers {
|
|
|
|
if v == handler {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// If the new mount point ends in / and another mount point
|
|
|
|
// shares the same prefix, remove the other handler.
|
|
|
|
// (e.g. /foo/ overwrites /foo)
|
|
|
|
// The opposite example is also handled.
|
|
|
|
m1 := strings.TrimSuffix(mount, "/")
|
|
|
|
m2 := strings.TrimSuffix(k, "/")
|
|
|
|
if m1 == m2 {
|
|
|
|
delete(sc.Web[hp].Handlers, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetTCPForwarding sets the fwdAddr (IP:port form) to which to forward
|
|
|
|
// connections from the given port. If terminateTLS is true, TLS connections
|
|
|
|
// are terminated with only the given host name permitted before passing them
|
|
|
|
// to the fwdAddr.
|
|
|
|
func (sc *ServeConfig) SetTCPForwarding(port uint16, fwdAddr string, terminateTLS bool, host string) {
|
|
|
|
if sc == nil {
|
|
|
|
sc = new(ServeConfig)
|
|
|
|
}
|
|
|
|
mak.Set(&sc.TCP, port, &TCPPortHandler{TCPForward: fwdAddr})
|
|
|
|
if terminateTLS {
|
|
|
|
sc.TCP[port].TerminateTLS = host
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetFunnel sets the sc.AllowFunnel value for the given host and port.
|
|
|
|
func (sc *ServeConfig) SetFunnel(host string, port uint16, setOn bool) {
|
|
|
|
if sc == nil {
|
|
|
|
sc = new(ServeConfig)
|
|
|
|
}
|
|
|
|
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
|
|
|
|
|
|
|
|
// TODO(tylersmalley): should ensure there is no other conflicting funnel
|
|
|
|
// TODO(tylersmalley): add error handling for if toggling for existing sc
|
|
|
|
if setOn {
|
|
|
|
mak.Set(&sc.AllowFunnel, hp, true)
|
|
|
|
} else if _, exists := sc.AllowFunnel[hp]; exists {
|
|
|
|
delete(sc.AllowFunnel, hp)
|
|
|
|
// Clear map mostly for testing.
|
|
|
|
if len(sc.AllowFunnel) == 0 {
|
|
|
|
sc.AllowFunnel = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveWebHandler deletes the web handlers at all of the given mount points
|
|
|
|
// for the provided host and port in the serve config. If cleanupFunnel is
|
|
|
|
// true, this also removes the funnel value for this port if no handlers remain.
|
|
|
|
func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []string, cleanupFunnel bool) {
|
|
|
|
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
|
|
|
|
|
|
|
|
// Delete existing handler, then cascade delete if empty.
|
|
|
|
for _, m := range mounts {
|
|
|
|
delete(sc.Web[hp].Handlers, m)
|
|
|
|
}
|
|
|
|
if len(sc.Web[hp].Handlers) == 0 {
|
|
|
|
delete(sc.Web, hp)
|
|
|
|
delete(sc.TCP, port)
|
|
|
|
if cleanupFunnel {
|
|
|
|
delete(sc.AllowFunnel, hp) // disable funnel if no mounts remain for the port
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear empty maps, mostly for testing.
|
|
|
|
if len(sc.Web) == 0 {
|
|
|
|
sc.Web = nil
|
|
|
|
}
|
|
|
|
if len(sc.TCP) == 0 {
|
|
|
|
sc.TCP = nil
|
|
|
|
}
|
|
|
|
if len(sc.AllowFunnel) == 0 {
|
|
|
|
sc.AllowFunnel = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveTCPForwarding deletes the TCP forwarding configuration for the given
|
|
|
|
// port from the serve config.
|
|
|
|
func (sc *ServeConfig) RemoveTCPForwarding(port uint16) {
|
|
|
|
delete(sc.TCP, port)
|
|
|
|
if len(sc.TCP) == 0 {
|
|
|
|
sc.TCP = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-21 16:32:20 +00:00
|
|
|
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
|
|
|
|
// traffic for any host:port.
|
2023-04-05 02:20:27 +00:00
|
|
|
//
|
|
|
|
// View version of ServeConfig.IsFunnelOn.
|
|
|
|
func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() }
|
|
|
|
|
2023-06-21 16:32:20 +00:00
|
|
|
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
|
|
|
|
// traffic for any host:port.
|
2022-12-08 03:17:40 +00:00
|
|
|
func (sc *ServeConfig) IsFunnelOn() bool {
|
|
|
|
if sc == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, b := range sc.AllowFunnel {
|
|
|
|
if b {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
2022-11-19 14:42:14 +00:00
|
|
|
}
|
2023-03-10 19:44:28 +00:00
|
|
|
|
2023-03-11 16:45:40 +00:00
|
|
|
// CheckFunnelAccess checks whether Funnel access is allowed for the given node
|
|
|
|
// and port.
|
|
|
|
// It checks:
|
2024-03-05 18:54:37 +00:00
|
|
|
// 1. HTTPS is enabled on the tailnet
|
2023-08-09 14:06:58 +00:00
|
|
|
// 2. the node has the "funnel" nodeAttr
|
|
|
|
// 3. the port is allowed for Funnel
|
2023-03-10 19:44:28 +00:00
|
|
|
//
|
2023-09-28 06:01:09 +00:00
|
|
|
// The node arg should be the ipnstate.Status.Self node.
|
|
|
|
func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error {
|
2024-03-05 18:54:37 +00:00
|
|
|
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 {
|
2023-09-28 06:01:09 +00:00
|
|
|
if !node.HasCap(tailcfg.CapabilityHTTPS) {
|
2023-03-13 19:33:16 +00:00
|
|
|
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
|
2023-03-10 19:44:28 +00:00
|
|
|
}
|
2023-09-28 06:01:09 +00:00
|
|
|
if !node.HasCap(tailcfg.NodeAttrFunnel) {
|
2023-03-13 19:33:16 +00:00
|
|
|
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.")
|
2023-03-10 19:44:28 +00:00
|
|
|
}
|
2024-03-05 18:54:37 +00:00
|
|
|
return nil
|
2023-03-11 16:45:40 +00:00
|
|
|
}
|
|
|
|
|
2023-08-09 14:06:58 +00:00
|
|
|
// CheckFunnelPort checks whether the given port is allowed for Funnel.
|
2023-03-11 16:45:40 +00:00
|
|
|
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
|
|
|
|
// ports.
|
2023-09-28 06:01:09 +00:00
|
|
|
func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error {
|
2023-03-11 16:45:40 +00:00
|
|
|
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
|
2023-09-28 06:01:09 +00:00
|
|
|
parseAttr := func(attr string) (string, error) {
|
2023-03-11 16:45:40 +00:00
|
|
|
u, err := url.Parse(attr)
|
|
|
|
if err != nil {
|
2023-09-28 06:01:09 +00:00
|
|
|
return "", deny("")
|
2023-03-11 16:45:40 +00:00
|
|
|
}
|
2023-09-28 06:01:09 +00:00
|
|
|
portsStr := u.Query().Get("ports")
|
2023-03-11 16:45:40 +00:00
|
|
|
if portsStr == "" {
|
2023-09-28 06:01:09 +00:00
|
|
|
return "", deny("")
|
2023-03-11 16:45:40 +00:00
|
|
|
}
|
|
|
|
u.RawQuery = ""
|
2023-09-06 17:17:25 +00:00
|
|
|
if u.String() != string(tailcfg.CapabilityFunnelPorts) {
|
2023-09-28 06:01:09 +00:00
|
|
|
return "", deny("")
|
|
|
|
}
|
|
|
|
return portsStr, nil
|
|
|
|
}
|
|
|
|
for attr := range node.CapMap {
|
|
|
|
attr := string(attr)
|
|
|
|
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
portsStr, err = parseAttr(attr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if portsStr == "" {
|
|
|
|
for _, attr := range node.Capabilities {
|
|
|
|
attr := string(attr)
|
|
|
|
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
portsStr, err = parseAttr(attr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
break
|
2023-03-11 16:45:40 +00:00
|
|
|
}
|
|
|
|
}
|
2023-09-28 06:01:09 +00:00
|
|
|
if portsStr == "" {
|
|
|
|
return deny("")
|
|
|
|
}
|
2023-03-11 16:45:40 +00:00
|
|
|
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)
|
2023-03-10 19:44:28 +00:00
|
|
|
}
|
2023-09-05 17:51:52 +00:00
|
|
|
|
2024-03-05 18:54:37 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-09-05 17:51:52 +00:00
|
|
|
// 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.
|
|
|
|
func (v ServeConfigView) RangeOverTCPs(f func(port uint16, _ TCPPortHandlerView) bool) {
|
|
|
|
parentCont := true
|
|
|
|
v.TCP().Range(func(k uint16, v TCPPortHandlerView) (cont bool) {
|
|
|
|
parentCont = f(k, v)
|
|
|
|
return parentCont
|
|
|
|
})
|
|
|
|
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
|
|
|
|
if !parentCont {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
v.TCP().Range(func(k uint16, v TCPPortHandlerView) (cont bool) {
|
|
|
|
parentCont = f(k, v)
|
|
|
|
return parentCont
|
|
|
|
})
|
|
|
|
return parentCont
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// RangeOverWebs ranges over both background and foreground Webs.
|
|
|
|
// If the returned bool from the given f is false, then this function stops
|
|
|
|
// iterating immediately and does not check other foreground configs.
|
|
|
|
func (v ServeConfigView) RangeOverWebs(f func(_ HostPort, conf WebServerConfigView) bool) {
|
|
|
|
parentCont := true
|
|
|
|
v.Web().Range(func(k HostPort, v WebServerConfigView) (cont bool) {
|
|
|
|
parentCont = f(k, v)
|
|
|
|
return parentCont
|
|
|
|
})
|
|
|
|
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
|
|
|
|
if !parentCont {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
v.Web().Range(func(k HostPort, v WebServerConfigView) (cont bool) {
|
|
|
|
parentCont = f(k, v)
|
|
|
|
return parentCont
|
|
|
|
})
|
|
|
|
return parentCont
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindTCP returns the first TCP that matches with the given port. It
|
|
|
|
// prefers a foreground match first followed by a background search if none
|
|
|
|
// existed.
|
|
|
|
func (v ServeConfigView) FindTCP(port uint16) (res TCPPortHandlerView, ok bool) {
|
|
|
|
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
|
|
|
|
res, ok = v.TCP().GetOk(port)
|
|
|
|
return !ok
|
|
|
|
})
|
|
|
|
if ok {
|
|
|
|
return res, ok
|
|
|
|
}
|
|
|
|
return v.TCP().GetOk(port)
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindWeb returns the first Web that matches with the given HostPort. It
|
|
|
|
// prefers a foreground match first followed by a background search if none
|
|
|
|
// existed.
|
|
|
|
func (v ServeConfigView) FindWeb(hp HostPort) (res WebServerConfigView, ok bool) {
|
|
|
|
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
|
|
|
|
res, ok = v.Web().GetOk(hp)
|
|
|
|
return !ok
|
|
|
|
})
|
|
|
|
if ok {
|
|
|
|
return res, ok
|
|
|
|
}
|
|
|
|
return v.Web().GetOk(hp)
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasAllowFunnel returns whether this config has at least one AllowFunnel
|
|
|
|
// set in the background or foreground configs.
|
|
|
|
func (v ServeConfigView) HasAllowFunnel() bool {
|
|
|
|
return v.AllowFunnel().Len() > 0 || func() bool {
|
|
|
|
var exists bool
|
|
|
|
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
|
|
|
|
exists = v.AllowFunnel().Len() > 0
|
|
|
|
return !exists
|
|
|
|
})
|
|
|
|
return exists
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindFunnel reports whether target exists in in either the background AllowFunnel
|
|
|
|
// or any of the foreground configs.
|
|
|
|
func (v ServeConfigView) HasFunnelForTarget(target HostPort) bool {
|
|
|
|
if v.AllowFunnel().Get(target) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
var exists bool
|
|
|
|
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
|
|
|
|
if exists = v.AllowFunnel().Get(target); exists {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
return exists
|
|
|
|
}
|