diff options
| author | Irbe Krumina <irbe@tailscale.com> | 2025-01-04 08:02:12 +0000 |
|---|---|---|
| committer | Irbe Krumina <irbe@tailscale.com> | 2025-01-04 08:02:29 +0000 |
| commit | 5062c9be342625a5eae29053dd6d6a6000d1c066 (patch) | |
| tree | 6c47f982472194816ac964677f9a23abb94a60fe | |
| parent | ff095606ccff083160eb01a8a4cc062cacfe1a33 (diff) | |
| download | tailscale-irbekrm/udp_fwd.tar.xz tailscale-irbekrm/udp_fwd.zip | |
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
| -rw-r--r-- | cmd/k8s-operator/wip.md | 18 | ||||
| -rw-r--r-- | ipn/doc.go | 2 | ||||
| -rw-r--r-- | ipn/ipn_clone.go | 27 | ||||
| -rw-r--r-- | ipn/ipn_view.go | 61 | ||||
| -rw-r--r-- | ipn/ipnlocal/local.go | 70 | ||||
| -rw-r--r-- | ipn/ipnlocal/serve.go | 235 | ||||
| -rw-r--r-- | ipn/serve.go | 63 | ||||
| -rw-r--r-- | wgengine/netstack/netstack.go | 32 |
8 files changed, 491 insertions, 17 deletions
diff --git a/cmd/k8s-operator/wip.md b/cmd/k8s-operator/wip.md new file mode 100644 index 000000000..bad041be3 --- /dev/null +++ b/cmd/k8s-operator/wip.md @@ -0,0 +1,18 @@ +This is an experimental attempt to add UDP forwarding support to serve with a goal of being able to implement the Kubernetes Operator L3 proxies, that currently use iptables/nftables, in code using serve. + +This allows configuring a UDP backend for serve like so: + +```sh +{"UDP":{"53":{"UDPForward":"10.0.0.3:1053"}}} +``` + +where 53 is the port that will be exposed on the proxy and 10.0.0.3:1053 is the address and port of a Kubernetes Service. +There is already an existing TCPForward field that works the same way. + +The operator could generate these serve configs for proxies. + +This would allow deploying L3 proxies without needing to use privileged containers (if run in userspace) and would also allow users to benefit from some of the performance improvements in userspace. + +I've tested both TCP and UDP forwarding to Kubernetes Services both in tun mode and userspace mode and it works as expected. + +Kubernetes Services also support SCTP, but it is not widely used. diff --git a/ipn/doc.go b/ipn/doc.go index 9a0bbb800..cbfb7558f 100644 --- a/ipn/doc.go +++ b/ipn/doc.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:generate go run tailscale.com/cmd/viewer -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig +//go:generate go run tailscale.com/cmd/viewer -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,UDPPortHandler,HTTPHandler,WebServerConfig // Package ipn implements the interactions between the Tailscale cloud // control plane and the local network stack. diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 34d7ba9a6..26c541cc2 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -95,6 +95,16 @@ func (src *ServeConfig) Clone() *ServeConfig { } } } + if dst.UDP != nil { + dst.UDP = map[uint16]*UDPPortHandler{} + for k, v := range src.UDP { + if v == nil { + dst.UDP[k] = nil + } else { + dst.UDP[k] = ptr.To(*v) + } + } + } if dst.Web != nil { dst.Web = map[HostPort]*WebServerConfig{} for k, v := range src.Web { @@ -132,6 +142,7 @@ func (src *ServeConfig) Clone() *ServeConfig { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct { TCP map[uint16]*TCPPortHandler + UDP map[uint16]*UDPPortHandler Web map[HostPort]*WebServerConfig Services map[string]*ServiceConfig AllowFunnel map[HostPort]bool @@ -196,6 +207,22 @@ var _TCPPortHandlerCloneNeedsRegeneration = TCPPortHandler(struct { TerminateTLS string }{}) +// Clone makes a deep copy of UDPPortHandler. +// The result aliases no memory with the original. +func (src *UDPPortHandler) Clone() *UDPPortHandler { + if src == nil { + return nil + } + dst := new(UDPPortHandler) + *dst = *src + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _UDPPortHandlerCloneNeedsRegeneration = UDPPortHandler(struct { + UDPForward string +}{}) + // Clone makes a deep copy of HTTPHandler. // The result aliases no memory with the original. func (src *HTTPHandler) Clone() *HTTPHandler { diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index bc67531e4..03c11f31b 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -18,7 +18,7 @@ import ( "tailscale.com/types/views" ) -//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig +//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,UDPPortHandler,HTTPHandler,WebServerConfig // View returns a readonly view of Prefs. func (p *Prefs) View() PrefsView { @@ -189,6 +189,12 @@ func (v ServeConfigView) TCP() views.MapFn[uint16, *TCPPortHandler, TCPPortHandl }) } +func (v ServeConfigView) UDP() views.MapFn[uint16, *UDPPortHandler, UDPPortHandlerView] { + return views.MapFnOf(v.ж.UDP, func(t *UDPPortHandler) UDPPortHandlerView { + return t.View() + }) +} + func (v ServeConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServerConfigView] { return views.MapFnOf(v.ж.Web, func(t *WebServerConfig) WebServerConfigView { return t.View() @@ -215,6 +221,7 @@ func (v ServeConfigView) ETag() string { return v.ж.ETag } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _ServeConfigViewNeedsRegeneration = ServeConfig(struct { TCP map[uint16]*TCPPortHandler + UDP map[uint16]*UDPPortHandler Web map[HostPort]*WebServerConfig Services map[string]*ServiceConfig AllowFunnel map[HostPort]bool @@ -345,6 +352,58 @@ var _TCPPortHandlerViewNeedsRegeneration = TCPPortHandler(struct { TerminateTLS string }{}) +// View returns a readonly view of UDPPortHandler. +func (p *UDPPortHandler) View() UDPPortHandlerView { + return UDPPortHandlerView{ж: p} +} + +// UDPPortHandlerView provides a read-only view over UDPPortHandler. +// +// Its methods should only be called if `Valid()` returns true. +type UDPPortHandlerView struct { + // ж is the underlying mutable value, named with a hard-to-type + // character that looks pointy like a pointer. + // It is named distinctively to make you think of how dangerous it is to escape + // to callers. You must not let callers be able to mutate it. + ж *UDPPortHandler +} + +// Valid reports whether underlying value is non-nil. +func (v UDPPortHandlerView) Valid() bool { return v.ж != nil } + +// AsStruct returns a clone of the underlying value which aliases no memory with +// the original. +func (v UDPPortHandlerView) AsStruct() *UDPPortHandler { + if v.ж == nil { + return nil + } + return v.ж.Clone() +} + +func (v UDPPortHandlerView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +func (v *UDPPortHandlerView) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("already initialized") + } + if len(b) == 0 { + return nil + } + var x UDPPortHandler + if err := json.Unmarshal(b, &x); err != nil { + return err + } + v.ж = &x + return nil +} + +func (v UDPPortHandlerView) UDPForward() string { return v.ж.UDPForward } + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _UDPPortHandlerViewNeedsRegeneration = UDPPortHandler(struct { + UDPForward string +}{}) + // View returns a readonly view of HTTPHandler. func (p *HTTPHandler) View() HTTPHandlerView { return HTTPHandlerView{ж: p} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index d6daf3535..2b59be3be 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -229,6 +229,7 @@ type LocalBackend struct { filterAtomic atomic.Pointer[filter.Filter] containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool] shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool] + shouldInterceptUDPPortAtomic syncs.AtomicValue[func(uint16) bool] numClientStatusCalls atomic.Uint32 // The mutex protects the following elements. @@ -317,8 +318,9 @@ type LocalBackend struct { webClient webClient webClientListeners map[netip.AddrPort]*localListener // listeners for local web client traffic - serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic - serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy + serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic + serveUDPListeners map[netip.AddrPort]*localUDPListener // listeners for local serve UDP traffic + serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy // statusLock must be held before calling statusChanged.Wait() or // statusChanged.Broadcast(). @@ -378,6 +380,9 @@ type LocalBackend struct { // backend is healthy and captive portal detection is not required // (sending false). needsCaptiveDetection chan bool + + // New field for tracking UDP ports to intercept + udpPortsToIntercept map[uint16]struct{} } // HealthTracker returns the health tracker for the backend. @@ -483,6 +488,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo captiveCtx: captiveCtx, captiveCancel: nil, // so that we start checkCaptivePortalLoop when Running needsCaptiveDetection: make(chan bool), + udpPortsToIntercept: make(map[uint16]struct{}), } mConn.SetNetInfoCallback(b.setNetInfo) @@ -2660,7 +2666,7 @@ func shrinkDefaultRoute(route netip.Prefix, localInterfaceRoutes *netipx.IPSet, } // readPoller is a goroutine that receives service lists from -// b.portpoll and propagates them into the controlclient's HostInfo. +// b.portpoll and propagates them into the controlclient's Hostinfo. func (b *LocalBackend) readPoller() { if !envknob.BoolDefaultTrue("TS_PORTLIST") { return @@ -3327,6 +3333,39 @@ func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) { } b.shouldInterceptTCPPortAtomic.Store(f) } +func (b *LocalBackend) setUDPPortsIntercepted(ports []uint16) { + slices.Sort(ports) + uniq.ModifySlice(&ports) + var f func(uint16) bool + switch len(ports) { + case 0: + f = func(uint16) bool { return false } + case 1: + f = func(p uint16) bool { return ports[0] == p } + case 2: + f = func(p uint16) bool { return ports[0] == p || ports[1] == p } + case 3: + f = func(p uint16) bool { return ports[0] == p || ports[1] == p || ports[2] == p } + default: + if len(ports) > 16 { + m := map[uint16]bool{} + for _, p := range ports { + m[p] = true + } + f = func(p uint16) bool { return m[p] } + } else { + f = func(p uint16) bool { + for _, x := range ports { + if p == x { + return true + } + } + return false + } + } + } + b.shouldInterceptUDPPortAtomic.Store(f) +} // setAtomicValuesFromPrefsLocked populates sshAtomicBool, containsViaIPFuncAtomic, // shouldInterceptTCPPortAtomic, and exposeRemoteWebClientAtomicBool from the prefs p, @@ -4104,7 +4143,12 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c } return nil, nil } - +func (b *LocalBackend) UDPHandlerForDst(src, dst netip.AddrPort) (handler func(c net.PacketConn) error) { + if handler := b.udpHandlerForServe(dst.Port(), src); handler != nil { + return handler + } + return nil +} func (b *LocalBackend) handleDriveConn(conn net.Conn) error { fs, ok := b.sys.DriveForLocal.GetOK() if !ok || !b.DriveAccessEnabled() { @@ -5882,6 +5926,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn. } b.reloadServeConfigLocked(prefs) + serveUDPPorts := make([]uint16, 0, 3) if b.serveConfig.Valid() { servePorts := make([]uint16, 0, 3) b.serveConfig.RangeOverTCPs(func(port uint16, _ ipn.TCPPortHandlerView) bool { @@ -5898,6 +5943,15 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn. if !b.sys.IsNetstack() { b.updateServeTCPPortNetMapAddrListenersLocked(servePorts) } + b.serveConfig.RangeOverUDPs(func(port uint16, _ ipn.UDPPortHandlerView) bool { + if port > 0 { + serveUDPPorts = append(serveUDPPorts, uint16(port)) + } + return true + }) + if !b.sys.IsNetstack() { + b.updateServeUDPPortNetMapAddrListenersLocked(serveUDPPorts) + } } // Kick off a Hostinfo update to control if WireIngress changed. if wire := b.wantIngressLocked(); b.hostinfo != nil && b.hostinfo.WireIngress != wire { @@ -5907,6 +5961,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn. } b.setTCPPortsIntercepted(handlePorts) + b.setUDPPortsIntercepted(serveUDPPorts) } // setServeProxyHandlersLocked ensures there is an http proxy handler for each @@ -6740,6 +6795,13 @@ func (b *LocalBackend) ShouldInterceptTCPPort(port uint16) bool { return b.shouldInterceptTCPPortAtomic.Load()(port) } +// ShouldInterceptUDPPort reports whether the given UDP port number to a +// Tailscale IP (not a subnet router, service IP, etc) should be intercepted by +// Tailscaled and handled in-process. +func (b *LocalBackend) ShouldInterceptUDPPort(port uint16) bool { + return b.shouldInterceptUDPPortAtomic.Load()(port) +} + // SwitchProfile switches to the profile with the given id. // It will restart the backend on success. // If the profile is not known, it returns an errProfileNotFound. diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 61bed0552..cc355c16a 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -149,7 +149,7 @@ func (s *localListener) Run() { // On macOS, we need to bind to ""/all-interfaces due to // the network sandbox. Ideally we would only bind to the // Tailscale interface, but macOS errors out if we try to - // to listen on privileged ports binding only to a specific + // listen on privileged ports binding only to a specific // interface. (#6364) ipStr = "" } @@ -936,3 +936,236 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHe return &cert, nil } } + +// localUDPListener is the state of host-level net.ListenPacket for a specific (Tailscale IP, port) +// combination. It is used to handle UDP traffic. +type localUDPListener struct { + b *LocalBackend + ap netip.AddrPort + ctx context.Context // valid while listener is desired + cancel context.CancelFunc // for ctx, to close listener + logf logger.Logf + bo *backoff.Backoff // for retrying failed Listen calls + + handler func(net.PacketConn) error // handler for inbound packets + closeListener syncs.AtomicValue[func() error] // Listener's Close method, if any +} + +func (b *LocalBackend) newServeUDPListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *localUDPListener { + ctx, cancel := context.WithCancel(ctx) + return &localUDPListener{ + b: b, + ap: ap, + ctx: ctx, + cancel: cancel, + logf: logf, + handler: func(conn net.PacketConn) error { + srcAddr := conn.LocalAddr().(*net.UDPAddr).AddrPort() + handler := b.udpHandlerForServe(ap.Port(), srcAddr) + if handler == nil { + b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, ap.Port()) + conn.Close() + return nil + } + return handler(conn) + }, + bo: backoff.NewBackoff("serve-udp-listener", logf, 30*time.Second), + } +} + +// Close cancels the context and closes the listener, if any. +func (s *localUDPListener) Close() error { + s.cancel() + if close, ok := s.closeListener.LoadOk(); ok { + s.closeListener.Store(nil) + close() + } + return nil +} + +// Run starts a net.ListenPacket for the localUDPListener's address and port. +// If unable to listen, it retries with exponential backoff. +// Listen is retried until the context is canceled. +func (s *localUDPListener) Run() { + for { + ip := s.ap.Addr() + ipStr := ip.String() + + var lc net.ListenConfig + if initListenConfig != nil { + if err := initListenConfig(&lc, ip, s.b.prevIfState, s.b.dialer.TUNName()); err != nil { + s.logf("localUDPListener failed to init listen config %v, backing off: %v", s.ap, err) + s.bo.BackOff(s.ctx, err) + continue + } + } + + // while we were backing off and trying again, the context got canceled + // so don't bind, just return, because otherwise there will be no way + // to close this listener + if s.ctx.Err() != nil { + s.logf("localUDPListener context closed before binding") + return + } + + conn, err := lc.ListenPacket(s.ctx, "udp", net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port()))) + if err != nil { + if s.shouldWarnAboutListenError(err) { + s.logf("localUDPListener failed to listen on %v, backing off: %v", s.ap, err) + } + s.bo.BackOff(s.ctx, err) + continue + } + s.closeListener.Store(conn.Close) + + s.logf("listening on %v", s.ap) + err = s.handler(conn) + if s.ctx.Err() != nil { + // context canceled, we're done + return + } + if err != nil { + s.logf("localUDPListener handler error, retrying: %v", err) + } + } +} + +func (s *localUDPListener) shouldWarnAboutListenError(err error) bool { + if !s.b.sys.NetMon.Get().InterfaceState().HasIP(s.ap.Addr()) { + return false + } + return true +} + +// udpHandlerForServe returns a handler for a UDP connection to be served via +// the ipn.ServeConfig. +func (b *LocalBackend) udpHandlerForServe(port uint16, src netip.AddrPort) func(conn net.PacketConn) error { + if h, ok := b.serveConfig.FindUDP(port); ok { + return func(conn net.PacketConn) error { + // TODO: buffer size? + buf := make([]byte, 4096) + remoteAddr := h.UDPForward() + rAddrPort, err := netip.ParseAddrPort(remoteAddr) + if err != nil { + b.logf("localbackend: error parsing remote address: %v", err) + return nil + } + udpAddr := net.UDPAddrFromAddrPort(rAddrPort) + srcAddr := net.UDPAddrFromAddrPort(src) + + // Instead of using net.ListenPacket, we use netns.NewDialer to create a connection to the remote address + // Create a connection to the remote address + rconn, err := net.ListenPacket("udp", ":0") + if err != nil { + b.logf("localbackend: error creating remote UDP connection: %v", err) + return nil + } + defer rconn.Close() + + // Create a context for cleanup + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + wg.Add(2) + + // Forward from conn to remote + go func() { + defer wg.Done() + for { + n, _, err := conn.ReadFrom(buf) + if err != nil { + if ctx.Err() == nil { + b.logf("localbackend: error reading from UDP connection: %v", err) + } + cancel() + return + } + _, err = rconn.WriteTo(buf[:n], udpAddr) + if err != nil { + if ctx.Err() == nil { + b.logf("localbackend: error forwarding UDP packet to %s: %v", remoteAddr, err) + } + cancel() + return + } + } + }() + + // Forward from remote back to conn + go func() { + defer wg.Done() + buf := make([]byte, 4096) + for { + n, _, err := rconn.ReadFrom(buf) + if err != nil { + if ctx.Err() == nil { + b.logf("localbackend: error reading from remote UDP connection: %v", err) + } + cancel() + return + } + _, err = conn.WriteTo(buf[:n], srcAddr) + if err != nil { + if ctx.Err() == nil { + b.logf("localbackend: error forwarding UDP packet back to source: %v", err) + } + cancel() + return + } + } + }() + + wg.Wait() + return nil + } + } + return nil +} + +// updateServeUDPPortNetMapAddrListenersLocked starts a net.ListenPacket for configured +// UDP Serve ports on all the node's addresses. +// Existing Listeners are closed if port no longer in incoming ports list. +// +// b.mu must be held. +func (b *LocalBackend) updateServeUDPPortNetMapAddrListenersLocked(ports []uint16) { + // Create a list of listeners to close first + var toClose []netip.AddrPort + for ap, sl := range b.serveUDPListeners { + if !slices.Contains(ports, ap.Port()) { + b.logf("closing UDP listener %v", ap) + sl.Close() + toClose = append(toClose, ap) + } + } + + // Now delete them from the map + for _, ap := range toClose { + delete(b.serveUDPListeners, ap) + } + + nm := b.netMap + if nm == nil { + b.logf("netMap is nil") + return + } + if !nm.SelfNode.Valid() { + b.logf("netMap SelfNode is nil") + return + } + + addrs := nm.GetAddresses() + for _, a := range addrs.All() { + for _, p := range ports { + addrPort := netip.AddrPortFrom(a.Addr(), p) + if _, ok := b.serveUDPListeners[addrPort]; ok { + continue // already listening + } + + sl := b.newServeUDPListener(context.Background(), addrPort, b.logf) + mak.Set(&b.serveUDPListeners, addrPort, sl) + + go sl.Run() + } + } +} diff --git a/ipn/serve.go b/ipn/serve.go index 49e0d9fa3..be8f20390 100644 --- a/ipn/serve.go +++ b/ipn/serve.go @@ -48,6 +48,10 @@ type ServeConfig struct { // the Tailscale IP addresses. (not subnet routers, etc) TCP map[uint16]*TCPPortHandler `json:",omitempty"` + // UDP are the list of UDP port numbers that tailscaled should handle for + // the Tailscale IP addresses. + UDP map[uint16]*UDPPortHandler `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"` @@ -148,6 +152,12 @@ type TCPPortHandler struct { TerminateTLS string `json:",omitempty"` } +// UDPPortHandler describes what to do when handling a UDP connection. +type UDPPortHandler struct { + // UDPForward is the IP:port to forward UDP packets to. + UDPForward string `json:",omitempty"` +} + // HTTPHandler is either a path or a proxy to serve. type HTTPHandler struct { // Exactly one of the following may be set. @@ -380,6 +390,24 @@ func (sc *ServeConfig) RemoveTCPForwarding(port uint16) { } } +// SetUDPForwarding sets the fwdAddr (IP:port form) to which to forward +// UDP packets from the given port. +func (sc *ServeConfig) SetUDPForwarding(port uint16, fwdAddr string) { + if sc == nil { + sc = new(ServeConfig) + } + mak.Set(&sc.UDP, port, &UDPPortHandler{UDPForward: fwdAddr}) +} + +// RemoveUDPForwarding deletes the UDP forwarding configuration for the given +// port from the serve config. +func (sc *ServeConfig) RemoveUDPForwarding(port uint16) { + delete(sc.UDP, port) + if len(sc.UDP) == 0 { + sc.UDP = nil + } +} + // IsFunnelOn reports whether if ServeConfig is currently allowing funnel // traffic for any host:port. // @@ -662,3 +690,38 @@ func (v ServeConfigView) HasFunnelForTarget(target HostPort) bool { }) return exists } + +// RangeOverUDPs ranges over both background and foreground UDPs. +// 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) RangeOverUDPs(f func(port uint16, _ UDPPortHandlerView) bool) { + parentCont := true + v.UDP().Range(func(k uint16, v UDPPortHandlerView) (cont bool) { + parentCont = f(k, v) + return parentCont + }) + v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) { + if !parentCont { + return false + } + v.UDP().Range(func(k uint16, v UDPPortHandlerView) (cont bool) { + parentCont = f(k, v) + return parentCont + }) + return parentCont + }) +} + +// FindUDP returns the first UDP that matches with the given port. It +// prefers a foreground match first followed by a background search if none +// existed. +func (v ServeConfigView) FindUDP(port uint16) (res UDPPortHandlerView, ok bool) { + v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) { + res, ok = v.UDP().GetOk(port) + return !ok + }) + if ok { + return res, ok + } + return v.UDP().GetOk(port) +} diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 20eac06e6..b840ba9aa 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -981,24 +981,29 @@ func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool { } else { peerAPIPort = uint16(ns.peerAPIPortAtomic(dstIP).Load()) } - dport := p.Dst.Port() - if dport == peerAPIPort { + + if peerAPIPort != 0 && p.Dst.Port() == peerAPIPort { return true } - // Also handle SSH connections, webserver, etc, if enabled: - if ns.lb.ShouldInterceptTCPPort(dport) { + + // Handle TCP connections for ports configured in ServeConfig + if ns.lb.ShouldInterceptTCPPort(p.Dst.Port()) { return true } } - if p.IPVersion == 6 && !isLocal && viaRange.Contains(dstIP) { - return ns.lb != nil && ns.lb.ShouldHandleViaIP(dstIP) - } - if ns.ProcessLocalIPs && isLocal { - return true + + // Handle UDP packets to the Tailscale IP(s) for ports configured in ServeConfig + if ns.lb != nil && p.IPProto == ipproto.UDP && isLocal { + if ns.lb.ShouldInterceptUDPPort(p.Dst.Port()) { + return true + } } - if ns.ProcessSubnets && !isLocal { + + // Handle 4via6 packets + if p.IPVersion == 6 && viaRange.Contains(dstIP) && ns.lb != nil && ns.lb.ShouldHandleViaIP(dstIP) { return true } + return false } @@ -1505,6 +1510,13 @@ func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) { return // Only MagicDNS and loopback traffic runs on the service IPs for now. } } + if ns.lb != nil { + handler := ns.lb.UDPHandlerForDst(srcAddr, dstAddr) + if handler != nil { + go handler(gonet.NewUDPConn(&wq, ep)) + return + } + } if get := ns.GetUDPHandlerForFlow; get != nil { h, intercept := get(srcAddr, dstAddr) |
