| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package ipn
- import (
- "errors"
- "fmt"
- "iter"
- "net"
- "net/netip"
- "net/url"
- "slices"
- "strconv"
- "strings"
- "tailscale.com/ipn/ipnstate"
- "tailscale.com/tailcfg"
- "tailscale.com/types/ipproto"
- "tailscale.com/util/dnsname"
- "tailscale.com/util/mak"
- "tailscale.com/util/set"
- )
- // ServeConfigKey returns a StateKey that stores the
- // JSON-encoded ServeConfig for a config profile.
- func ServeConfigKey(profileID ProfileID) StateKey {
- return StateKey("_serve/" + profileID)
- }
- // ServiceConfig contains the config information for a single service.
- // it contains a bool to indicate if the service is in Tun mode (L3 forwarding).
- // If the service is not in Tun mode, the service is configured by the L4 forwarding
- // (TCP ports) and/or the L7 forwarding (http handlers) information.
- type ServiceConfig 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"`
- // Tun determines if the service should be using L3 forwarding (Tun mode).
- Tun bool `json:",omitempty"`
- }
- // 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"`
- // Services maps from service name (in the form "svc:dns-label") to a ServiceConfig.
- // Which describes the L3, L4, and L7 forwarding information for the service.
- Services map[tailcfg.ServiceName]*ServiceConfig `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"`
- // 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 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. In practice this contains any serve config set via 'tailscale
- // serve' command run without the '--bg' flag. ServeConfig contained by Foreground is not expected itself to contain
- // another Foreground block.
- Foreground map[string]*ServeConfig `json:",omitempty"`
- // 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:"-"`
- }
- // 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
- // 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
- }
- // 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
- }
- // 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"`
- // 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"`
- // 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"`
- // ProxyProtocol indicates whether to send a PROXY protocol header
- // before forwarding the connection to TCPForward.
- //
- // This is only valid if TCPForward is non-empty.
- ProxyProtocol int `json:",omitzero"`
- }
- // 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)
- AcceptAppCaps []tailcfg.PeerCapability `json:",omitempty"` // peer capabilities to forward in grant header, e.g. example.com/cap/mon
- // Redirect, if not empty, is the target URL to redirect requests to.
- // By default, we redirect with HTTP 302 (Found) status.
- // If Redirect starts with '<httpcode>:', then we use that status instead.
- //
- // The target URL supports the following expansion variables:
- // - ${HOST}: replaced with the request's Host header value
- // - ${REQUEST_URI}: replaced with the request's full URI (path and query string)
- Redirect string `json:",omitempty"`
- // TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
- // temporary ones? Error codes?
- }
- // WebHandlerExists reports whether if the ServeConfig Web handler exists for
- // the given host:port and mount point.
- func (sc *ServeConfig) WebHandlerExists(svcName tailcfg.ServiceName, hp HostPort, mount string) bool {
- h := sc.GetWebHandler(svcName, 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(svcName tailcfg.ServiceName, hp HostPort, mount string) *HTTPHandler {
- if sc == nil {
- return nil
- }
- if svcName != "" {
- if svc, ok := sc.Services[svcName]; ok && svc.Web != nil {
- if webCfg, ok := svc.Web[hp]; ok {
- return webCfg.Handlers[mount]
- }
- }
- return nil
- }
- if sc.Web[hp] == nil {
- return nil
- }
- return sc.Web[hp].Handlers[mount]
- }
- // GetTCPPortHandler returns the TCPPortHandler for the given port. If the port
- // is not configured, nil is returned. Parameter svcName can be tailcfg.NoService
- // for local serve or a service name for a service hosted on node.
- func (sc *ServeConfig) GetTCPPortHandler(port uint16, svcName tailcfg.ServiceName) *TCPPortHandler {
- if sc == nil {
- return nil
- }
- if svcName != "" {
- if svc, ok := sc.Services[svcName]; ok && svc != nil {
- return svc.TCP[port]
- }
- return nil
- }
- return sc.TCP[port]
- }
- // 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.Services != nil {
- for _, serviceConfig := range sc.Services {
- if serviceConfig.Web != nil {
- for _, webServerConfig := range serviceConfig.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
- }
- // IsTCPForwardingAny reports whether ServeConfig is currently forwarding in
- // TCPForward mode on any port. This is exclusive of Web/HTTPS serving.
- func (sc *ServeConfig) IsTCPForwardingAny() bool {
- if sc == nil || len(sc.TCP) == 0 {
- return false
- }
- for _, h := range sc.TCP {
- if h.TCPForward != "" {
- return true
- }
- }
- return false
- }
- // IsTCPForwardingOnPort reports whether ServeConfig is currently forwarding
- // in TCPForward mode on the given port for local or a service. svcName will
- // either be noService (empty string) for local serve or a serviceName for service
- // hosted on node. Notice TCPForwarding is exclusive with Web/HTTPS serving.
- func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16, svcName tailcfg.ServiceName) bool {
- if sc == nil {
- return false
- }
- if svcName != "" {
- svc, ok := sc.Services[svcName]
- if !ok || svc == nil {
- return false
- }
- if svc.TCP[port] == nil {
- return false
- }
- } else if sc.TCP[port] == nil {
- return false
- }
- return !sc.IsServingWeb(port, svcName)
- }
- // IsServingWeb reports whether ServeConfig is currently serving Web (HTTP/HTTPS)
- // on the given port for local or a service. svcName will be either tailcfg.NoService,
- // or a serviceName for service hosted on node. This is exclusive with TCPForwarding.
- func (sc *ServeConfig) IsServingWeb(port uint16, svcName tailcfg.ServiceName) bool {
- return sc.IsServingHTTP(port, svcName) || sc.IsServingHTTPS(port, svcName)
- }
- // IsServingHTTPS reports whether ServeConfig is currently serving HTTPS on
- // the given port for local or a service. svcName will be either tailcfg.NoService
- // for local serve, or a serviceName for service hosted on node. This is exclusive
- // with HTTP and TCPForwarding.
- func (sc *ServeConfig) IsServingHTTPS(port uint16, svcName tailcfg.ServiceName) bool {
- if sc == nil {
- return false
- }
- var tcpHandlers map[uint16]*TCPPortHandler
- if svcName != "" {
- if svc := sc.Services[svcName]; svc != nil {
- tcpHandlers = svc.TCP
- }
- } else {
- tcpHandlers = sc.TCP
- }
- th := tcpHandlers[port]
- if th == nil {
- return false
- }
- return th.HTTPS
- }
- // IsServingHTTP reports whether ServeConfig is currently serving HTTP on the
- // given port for local or a service. svcName will be either tailcfg.NoService for
- // local serve, or a serviceName for service hosted on node. This is exclusive
- // with HTTPS and TCPForwarding.
- func (sc *ServeConfig) IsServingHTTP(port uint16, svcName tailcfg.ServiceName) bool {
- if sc == nil {
- return false
- }
- if svcName != "" {
- if svc := sc.Services[svcName]; svc != nil {
- if svc.TCP[port] != nil {
- return svc.TCP[port].HTTP
- }
- }
- return false
- }
- if sc.TCP[port] == nil {
- return false
- }
- 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
- }
- // 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. The st argument is needed when setting
- // a web handler for a service, otherwise it can be nil. mds is the Magic DNS
- // suffix, which is used to recreate serve's host.
- func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uint16, mount string, useTLS bool, mds string) {
- if sc == nil {
- sc = new(ServeConfig)
- }
- tcpMap := &sc.TCP
- webServerMap := &sc.Web
- hostName := host
- if svcName := tailcfg.AsServiceName(host); svcName != "" {
- hostName = strings.Join([]string{svcName.WithoutPrefix(), mds}, ".")
- svc, ok := sc.Services[svcName]
- if !ok {
- svc = new(ServiceConfig)
- mak.Set(&sc.Services, svcName, svc)
- }
- tcpMap = &svc.TCP
- webServerMap = &svc.Web
- }
- mak.Set(tcpMap, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
- hp := HostPort(net.JoinHostPort(hostName, strconv.Itoa(int(port))))
- webCfg, ok := (*webServerMap)[hp]
- if !ok {
- webCfg = new(WebServerConfig)
- mak.Set(webServerMap, hp, webCfg)
- }
- mak.Set(&webCfg.Handlers, mount, handler)
- // TODO(tylersmalley): handle multiple web handlers from foreground mode
- for k, v := range webCfg.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(webCfg.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.
- //
- // If proxyProtocol is non-zero, the corresponding PROXY protocol version
- // header is sent before forwarding the connection.
- func (sc *ServeConfig) SetTCPForwarding(port uint16, fwdAddr string, terminateTLS bool, proxyProtocol int, host string) {
- if sc == nil {
- sc = new(ServeConfig)
- }
- tcpPortHandler := &sc.TCP
- if svcName := tailcfg.AsServiceName(host); svcName != "" {
- svcConfig, ok := sc.Services[svcName]
- if !ok {
- svcConfig = new(ServiceConfig)
- mak.Set(&sc.Services, svcName, svcConfig)
- }
- tcpPortHandler = &svcConfig.TCP
- }
- handler := &TCPPortHandler{
- TCPForward: fwdAddr,
- ProxyProtocol: proxyProtocol, // can be 0
- }
- if terminateTLS {
- handler.TerminateTLS = host
- }
- mak.Set(tcpPortHandler, port, handler)
- }
- // 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 for the node (as opposed to a service).
- // 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
- }
- }
- // RemoveServiceWebHandler deletes the web handlers at all of the given mount points
- // for the provided host and port in the serve config for the given service.
- func (sc *ServeConfig) RemoveServiceWebHandler(svcName tailcfg.ServiceName, hostName string, port uint16, mounts []string) {
- hp := HostPort(net.JoinHostPort(hostName, strconv.Itoa(int(port))))
- svc, ok := sc.Services[svcName]
- if !ok || svc == nil {
- return
- }
- // Delete existing handler, then cascade delete if empty.
- for _, m := range mounts {
- delete(svc.Web[hp].Handlers, m)
- }
- if len(svc.Web[hp].Handlers) == 0 {
- delete(svc.Web, hp)
- delete(svc.TCP, port)
- }
- if len(svc.Web) == 0 && len(svc.TCP) == 0 {
- delete(sc.Services, svcName)
- }
- if len(sc.Services) == 0 {
- sc.Services = nil
- }
- }
- // RemoveTCPForwarding deletes the TCP forwarding configuration for the given
- // port from the serve config.
- func (sc *ServeConfig) RemoveTCPForwarding(svcName tailcfg.ServiceName, port uint16) {
- if svcName != "" {
- if svc := sc.Services[svcName]; svc != nil {
- delete(svc.TCP, port)
- if len(svc.TCP) == 0 {
- svc.TCP = nil
- }
- if len(svc.Web) == 0 && len(svc.TCP) == 0 {
- delete(sc.Services, svcName)
- }
- if len(sc.Services) == 0 {
- sc.Services = nil
- }
- }
- return
- }
- delete(sc.TCP, port)
- if len(sc.TCP) == 0 {
- sc.TCP = nil
- }
- }
- // IsFunnelOn reports whether if ServeConfig is currently allowing funnel
- // traffic for any host:port.
- //
- // View version of ServeConfig.IsFunnelOn.
- func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() }
- // IsFunnelOn reports whether any funnel endpoint is currently enabled for this node.
- func (sc *ServeConfig) IsFunnelOn() bool {
- if sc == nil {
- return false
- }
- for _, b := range sc.AllowFunnel {
- if b {
- return true
- }
- }
- for _, conf := range sc.Foreground {
- if conf.IsFunnelOn() {
- return true
- }
- }
- return false
- }
- // CheckFunnelAccess checks whether Funnel access is allowed for the given node
- // and port.
- // It checks:
- // 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 nil
- }
- // 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, node *ipnstate.PeerStatus) 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
- 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)
- 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.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 == "" {
- 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)
- }
- // 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. If it's for a service, remote addresses are allowed and
- // there doesn't have to be a port specified.
- //
- // examples:
- // - 3000
- // - localhost:3000
- // - tcp://localhost:3000
- // - http://localhost:3000
- // - https://localhost:3000
- // - https-insecure://localhost:3000
- // - https-insecure://localhost:3000/foo
- // - https://tailscale.com
- func ExpandProxyTargetValue(target string, supportedSchemes []string, defaultScheme string) (string, error) {
- const host = "127.0.0.1"
- // empty target is invalid
- if target == "" {
- return "", fmt.Errorf("empty target")
- }
- // 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
- }
- hasScheme := true
- // prepend scheme if not present
- if !strings.Contains(target, "://") {
- target = defaultScheme + "://" + target
- hasScheme = false
- }
- // 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 port according to host.
- if u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1" || u.Hostname() == "::1" {
- // require port for localhost targets
- if u.Port() == "" {
- return "", fmt.Errorf("port required for localhost target %q", target)
- }
- } else {
- validHN := dnsname.ValidHostname(u.Hostname()) == nil
- validIP := net.ParseIP(u.Hostname()) != nil
- if !validHN && !validIP {
- return "", fmt.Errorf("invalid hostname or IP address %q", u.Hostname())
- }
- // require scheme for non-localhost targets
- if !hasScheme {
- return "", fmt.Errorf("non-localhost target %q must include a scheme", target)
- }
- }
- port, err := strconv.ParseUint(u.Port(), 10, 16)
- if err != nil || port == 0 {
- if u.Port() == "" {
- return u.String(), nil // allow no port for remote destinations
- }
- return "", fmt.Errorf("invalid port %q", u.Port())
- }
- u.Host = fmt.Sprintf("%s:%d", u.Hostname(), port)
- return u.String(), nil
- }
- // TCPs returns an iterator over both background and foreground TCP
- // listeners.
- //
- // The key is the port number.
- func (v ServeConfigView) TCPs() iter.Seq2[uint16, TCPPortHandlerView] {
- return func(yield func(uint16, TCPPortHandlerView) bool) {
- for k, v := range v.TCP().All() {
- if !yield(k, v) {
- return
- }
- }
- for _, conf := range v.Foreground().All() {
- for k, v := range conf.TCP().All() {
- if !yield(k, v) {
- return
- }
- }
- }
- }
- }
- // Webs returns an iterator over both background and foreground Web configurations.
- func (v ServeConfigView) Webs() iter.Seq2[HostPort, WebServerConfigView] {
- return func(yield func(HostPort, WebServerConfigView) bool) {
- for k, v := range v.Web().All() {
- if !yield(k, v) {
- return
- }
- }
- for _, conf := range v.Foreground().All() {
- for k, v := range conf.Web().All() {
- if !yield(k, v) {
- return
- }
- }
- }
- for _, service := range v.Services().All() {
- for k, v := range service.Web().All() {
- if !yield(k, v) {
- return
- }
- }
- }
- }
- }
- // FindServiceTCP return the TCPPortHandlerView for the given service name and port.
- func (v ServeConfigView) FindServiceTCP(svcName tailcfg.ServiceName, port uint16) (res TCPPortHandlerView, ok bool) {
- svcCfg, ok := v.Services().GetOk(svcName)
- if !ok {
- return res, ok
- }
- return svcCfg.TCP().GetOk(port)
- }
- // FindServiceWeb returns the web handler for the service's host-port.
- func (v ServeConfigView) FindServiceWeb(svcName tailcfg.ServiceName, hp HostPort) (res WebServerConfigView, ok bool) {
- if svcCfg, ok := v.Services().GetOk(svcName); ok {
- if res, ok := svcCfg.Web().GetOk(hp); ok {
- return res, ok
- }
- }
- return res, ok
- }
- // 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) {
- res, ok = v.FindForegroundTCP(port)
- 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) {
- for _, conf := range v.Foreground().All() {
- if res, ok := conf.Web().GetOk(hp); ok {
- return res, ok
- }
- }
- return v.Web().GetOk(hp)
- }
- // FindForegroundTCP returns the first foreground TCP handler matching the input
- // port.
- func (v ServeConfigView) FindForegroundTCP(port uint16) (res TCPPortHandlerView, ok bool) {
- for _, conf := range v.Foreground().All() {
- if res, ok := conf.TCP().GetOk(port); ok {
- return res, ok
- }
- }
- return res, false
- }
- // HasAllowFunnel returns whether this config has at least one AllowFunnel
- // set in the background or foreground configs.
- func (v ServeConfigView) HasAllowFunnel() bool {
- if v.AllowFunnel().Len() > 0 {
- return true
- }
- for _, conf := range v.Foreground().All() {
- if conf.AllowFunnel().Len() > 0 {
- return true
- }
- }
- return false
- }
- // FindFunnel reports whether target exists 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
- }
- for _, conf := range v.Foreground().All() {
- if conf.AllowFunnel().Get(target) {
- return true
- }
- }
- return false
- }
- // ServicePortRange returns the list of tailcfg.ProtoPortRange that represents
- // the proto/ports pairs that are being served by the service.
- //
- // Right now Tun mode is the only thing supports UDP, otherwise serve only supports TCP.
- func (v ServiceConfigView) ServicePortRange() []tailcfg.ProtoPortRange {
- if v.Tun() {
- // If the service is in Tun mode, means service accept TCP/UDP on all ports.
- return []tailcfg.ProtoPortRange{{Ports: tailcfg.PortRangeAny}}
- }
- tcp := int(ipproto.TCP)
- // Deduplicate the ports.
- servePorts := make(set.Set[uint16])
- for port := range v.TCP().All() {
- if port > 0 {
- servePorts.Add(uint16(port))
- }
- }
- dedupedServePorts := servePorts.Slice()
- slices.Sort(dedupedServePorts)
- var ranges []tailcfg.ProtoPortRange
- for _, p := range dedupedServePorts {
- if n := len(ranges); n > 0 && p == ranges[n-1].Ports.Last+1 {
- ranges[n-1].Ports.Last = p
- continue
- }
- ranges = append(ranges, tailcfg.ProtoPortRange{
- Proto: tcp,
- Ports: tailcfg.PortRange{
- First: p,
- Last: p,
- },
- })
- }
- return ranges
- }
|