What magicsock is, in one sentence
The package comment at the top of wgengine/magicsock/magicsock.go says it plainly: magicsock “implements a socket that can change its communication path while in use, actively searching for the best way to communicate” (magicsock-go). Everything else in this module is elaboration on that sentence.
Here is the trick that makes Tailscale feel like magic. WireGuard, as a protocol and as the wireguard-go implementation Tailscale embeds, believes each peer lives at exactly one UDP address. It has no concept of “this peer might be reachable at five addresses, let’s race them.” Tailscale wants exactly that racing behavior, described end to end in the NAT traversal blog post: probe every candidate path, fall back to a relay instantly, upgrade to direct when a hole punch lands (nat-blog). The resolution is that magicsock implements wireguard-go’s conn.Bind interface, the abstraction wireguard-go uses to send and receive UDP. WireGuard hands magicsock a packet addressed to what it believes is a single stable endpoint, and magicsock decides, per send, whether those bytes leave via an IPv4 socket, an IPv6 socket, a DERP relay connection, or (since late 2025) a Geneve encapsulated peer relay path. Path selection is invisible to WireGuard by construction.
This guide walks the package as it exists on the main branch checked 2026-08-10. The file layout matters because it has drifted from what older writeups describe. The two canonical blog posts predate the current tree by years: how-blog is dated 2020-03-20 and nat-blog 2020-08-21, and neither one names a single source file or even uses the word magicsock. Meanwhile the code that once lived in one giant magicsock.go was broken apart in a run of commits on 2023-07-26 titled “factor out endpoint into its own file,” “factor out peerMap into separate file,” and “factor out more separable parts.” Today it reads as magicsock.go (the Conn, send and receive paths, disco dispatch), endpoint.go (per peer state and path selection), derp.go (relay integration), peermap.go (index structures), and relaymanager.go (peer relay path discovery, first landed in May 2025 ahead of the Peer Relays beta) (magicsock-pkg). If you are cross reading an old blog post against the source, expect names and locations to have moved; the mechanisms survived, the file boundaries and line numbers did not.
The Conn: one socket, many paths
Conn is the star. The comment above it reads “A Conn routes UDP packets and actively manages a list of its endpoints” (magicsock-go). Skim the struct definition and you can reconstruct the whole architecture from field names alone:
pconn4andpconn6, twoRebindingUDPConnvalues, the real IPv4 and IPv6 UDP sockets. “Rebinding” because magicsock can close and reopen them (network changes, sleep and wake) without WireGuard noticing.netChecker, anetcheck.Client, “the prober that discovers local network conditions” (magicsock-go). This is what runs STUN queries against the DERP fleet to learn your public ip:port mappings, the mechanism the NAT traversal post describes as asking a server “here’s the ip:port that I saw your UDP packet coming from” (nat-blog).peerMap, the index of every peer. Its own doc comment calls it “an index of peerInfos by node (WireGuard) key, disco key, and discovered ip:port endpoints”; the struct actually carries four maps, keyed by node key, node ID,epAddr(a source address seen on the wire), and disco key (peermap-go).derpMap,myDerp,activeDerp: the DERP region catalog from the control plane, your current home region ID (myDerp, “0 means none/unknown”), and a map of live DERP connections keyed by region (magicsock-go).relayManager, which “manages allocation and handshaking” of peer relay endpoints (magicsock-go, relaymanager-go).derpRoute, a map of “optional alternate routes to use as an optimization instead of contacting a peer via their home DERP connection,” remembered when a peer reaches us over some other DERP connection. The field and that comment live inmagicsock.go; thederpRoutetype itself is inderp.go(magicsock-go, derp-go).
The conn.Bind surface is implemented by a small wrapper type connBind, and Conn.Send carries the comment “Send implements conn.Bind” (magicsock-go). On the receive side, connBind registers multiple receive functions with wireguard-go: the IPv4 socket, the IPv6 socket, and receiveDERP, which drains a channel fed by DERP reader goroutines (magicsock-go, derp-go). To wireguard-go these are all just packet sources; it neither knows nor cares that one of them is a TCP stream to a relay.
The endpoint type: one peer, many candidate addresses
Move to endpoint.go. The doc comment on endpoint states the core inversion: “In wireguard-go and kernel WireGuard there is only one endpoint for a peer, but in Tailscale we distribute a number of possible endpoints for a peer” (endpoint-go). One endpoint object exists per peer, and it is simultaneously the conn.Endpoint handed to wireguard-go and the state machine that races paths. Its important fields, from current source:
publicKey: the peer’s node key, used for WireGuard and for addressing DERP frames.derpAddr: the peer’s DERP home, commented as the “fallback/bootstrap path” that is “non-zero for well-behaved clients” (endpoint-go). It is stored as a fakenetip.AddrPortwhose IP is the DERP magic IP and whose port is the region ID, so DERP destinations flow through the same address plumbing as real ones.bestAddr: anaddrQuality(address plus measured latency plus probed MTU), “best non-DERP path; zero if none” (endpoint-go).trustBestAddrUntil: the expiry time on that best path.endpointState: a map from each candidatenetip.AddrPortto its ping history and latency. Candidates arrive from the control plane netmap, from call-me-maybe messages, and from pings we receive.sentPing: outstanding disco pings by transaction ID, so pongs can be matched to the path they prove.isWireguardOnlyandrelayCapable: whether the peer is a plain WireGuard device with no disco (path selection degrades to latency picking among static addresses), and whether it can speak the peer relay protocol (endpoint-go).
The decision every data packet flows through is addrForSendLocked. Its logic, compressed from source: if bestAddr is set and trustBestAddrUntil has not passed, return the direct address alone. If the peer is WireGuard only, pick the lowest latency known candidate. Otherwise return both the (expired or missing) UDP address and derpAddr, meaning: send via DERP so the packet definitely arrives, optionally also via the stale direct path, and let discovery repair things (endpoint-go). One layer up, endpoint.send adds the kicker: whenever the chosen path is not a trusted direct one, it calls sendDiscoPingsLocked and, if the peer is relay capable, starts peer relay path discovery (endpoint-go). Discovery is not a background daemon that happens to exist; it is triggered by the very act of sending through a bad path. Traffic heals itself.
Disco: how pings and pongs elect a path
Disco is Tailscale’s discovery protocol, a small message family living in the disco package. Three of its messages carry the classic path discovery: Ping, Pong, and CallMeMaybe. Current source defines nine message types in total, the other six (BindUDPRelayEndpoint, BindUDPRelayEndpointChallenge, BindUDPRelayEndpointAnswer, CallMeMaybeVia, AllocateUDPRelayEndpointRequest, AllocateUDPRelayEndpointResponse) all belonging to the peer relay machinery, so a writeup that lists only three is describing the pre relay world (disco-go). Disco messages ride the same UDP sockets and DERP connections as data, but they are consumed inside magicsock and never surface to wireguard-go. In receiveIP, every incoming datagram is classified by packetLooksLike: disco packets route to handleDiscoMessage, STUN responses go to netcheck, and everything else is presumed WireGuard and passed up (magicsock-go).
The election cycle works like this, matching what the NAT traversal post describes at design level (nat-blog) but with the current function names:
- Sending through an untrusted path triggers
sendDiscoPingsLocked, which pings every plausible candidate inendpointStateand records each ping insentPingkeyed by transaction ID (endpoint-go). - If that round actually sent at least one ping and the peer has a DERP home, the same function queues a
CallMeMaybethrough DERP, the “I am about to ping you, ping me back at these addresses” message. The source states its purpose directly: inform the peer “that we’ve sent so our firewall ports are probably open and now would be a good time for them to connect.” It is not gated on whether the pair has ever talked directly before (endpoint-go).handleCallMeMaybeon the receiving side merges the advertised endpoints intoendpointStateand zeroes theirlastPingtimes “to force sendPingsLocked to send new ones” (endpoint-go). This mutual, near simultaneous pinging is the hole punch: both NATs see outbound traffic first and therefore allow the inbound reply (nat-blog). - A peer receiving a ping answers in
Conn.handlePingLockedwith aPongechoing the transaction ID and, crucially, the source ip:port it observed, and it opportunistically records the sender’s address mapping inpeerMap(magicsock-go). - The original sender’s
handlePongConnLockedmatches the pong to itssentPing, computes latency, and then decides promotion: the pong’s path becomesbestAddrifbetterAddrsays it wins, or unconditionally if the current best is untrusted. When the pong confirms the existing best path,trustBestAddrUntilis refreshed (endpoint-go).
betterAddr is worth reading in full because it encodes Tailscale’s path taste as arithmetic. Direct paths always beat peer relay paths (any address with a Geneve VNI set loses to one without). Then latency is scored as a percentage advantage, with bonus points layered on: loopback addresses get 50 points, link local 30, private addresses 20 (cheaper and more local than public), and IPv6 a 10 point nudge (endpoint-go).
Once a direct path is elected, a heartbeat keeps it warm: every heartbeatInterval (3 seconds) endpoint.heartbeat pings the current preferred path, and each pong on that same address slides trustBestAddrUntil forward by trustUDPAddrDuration (6.5 seconds). The heartbeat also retries the full candidate set, but wantFullPingLocked suppresses that once the current path is at or under goodEnoughLatency (5 milliseconds), and otherwise rate limits it to upgradeUDPDirectInterval (1 minute); below the good enough line, magicsock stops shopping. After sessionActiveTimeout (45 seconds) with no externally triggered send the heartbeat stops and the peering goes quiet (endpoint-go, magicsock-go: the four constants all live in magicsock.go, the logic in endpoint.go).
DERP inside magicsock
The DERP design story is in the blog: relays “blindly forward already-encrypted traffic,” private keys never leave the nodes, and the control plane stays out of the data path entirely (how-blog). Operationally, DERP servers are both the fallback data path and the signaling channel through which disco bootstraps direct connections (derp-kb, nat-blog). The implementation is derp.go.
The addressing trick is the part to internalize: a DERP destination is encoded as an ordinary netip.AddrPort whose IP is tailcfg.DerpMagicIPAddr and whose port is the region ID (derp-go). That is how one code path can hold “this peer’s fallback is DERP region 17” in the same field shape as “this peer is at an ip:port,” and why isDERP := addr.Addr() == tailcfg.DerpMagicIPAddr checks appear throughout magicsock.go. The magic IP never touches a wire; Conn.sendAddr intercepts it and hands the packet to derpWriteChanForRegion instead of a UDP socket.
Each active region connection gets two goroutines: runDerpReader, which receives frames and forwards them as derpReadResult values into derpRecvCh, and runDerpWriter, which drains a write channel into the HTTPS/TCP connection (derp-go). On the receive side, connBind.receiveDERP is registered with wireguard-go as just another receive function; processDERPReadResult labels each incoming frame with a synthetic source of DERP magic IP plus region, looks up the sending peer by the node key the relay authenticated, and hands the still encrypted WireGuard payload up the stack (derp-go). WireGuard decrypts as usual; end to end secrecy never depended on the relay.
Two refinements matter for field debugging. First, Conn.myDerp is your home region, chosen by netcheck latency measurements in maybeSetNearestDERP, and you keep a persistent connection to it so peers can always reach you there. Second, derpRoute records that a given peer recently reached us via some other region’s connection; fallbackDERPRegionForPeer uses it as a last resort so that even a peer whose netmap entry lacks endpoints can be answered over the DERP path they used to reach us (derp-go, magicsock-go). You can see that last ditch logic directly in endpoint.send: no UDP address and no DERP home means try fallbackDERPRegionForPeer before giving up with errNoUDPOrDERP (endpoint-go).
Peer relays and the relayManager (2025 and later)
Everything above existed in some form for years. The newest organ in magicsock is the peer relay path, announced as a public beta on 2025-10-29: Customer deployed relay nodes built into the ordinary Tailscale client, preferred above DERP but below direct, with measured throughput the announcement puts at “often multiple orders of magnitude higher than Tailscale’s managed DERP fleet” (peer-relays-blog, derp-kb). In the source, this is relaymanager.go plus threading through endpoint.go, present and active in the code as of the 2026-08-10 checkout; older descriptions of magicsock predate it entirely.
The relayManager doc comment defines its job: it “manages allocation, handshaking, and initial probing (disco ping/pong)” of relay server endpoints, running everything in a single runLoop goroutine fed by channels so it can be safely invoked while Conn.mu or endpoint.mu are held (relaymanager-go). Peer relay packets are ordinary UDP prefixed with an 8 byte Geneve header (RFC 8926) carrying a 3 byte virtual network identifier; that is why the address type throughout modern magicsock is epAddr, a netip.AddrPort plus optional VNI (endpoint-go). receiveIP decodes and strips the Geneve header before handing payloads to wireguard-go. Disco messages that arrive Geneve encapsulated get split two ways: handlePingLocked bails out early on any ping whose VNI is set and hands it to the relayManager, which is “always responsible for handling (replying) to Geneve-encapsulated [disco.Ping] messages,” while a Geneve encapsulated pong is first offered to the endpoints and only forwarded to the relayManager if no endpoint recognizes its transaction ID (magicsock-go).
Path discovery over relays deliberately reuses the disco election: the relayManager allocates a session on a candidate relay, handshakes, then probes it with disco pings, and only when a pong proves the path does endpoint.udpRelayEndpointReady consider installing it as bestAddr (endpoint-go, relaymanager-go). Because betterAddr ranks any VNI bearing path below any direct path, a working peer relay never blocks a later direct upgrade. Relay epAddr values are also kept out of endpointState on purpose: they are either the current best address or forgotten, with peerMap.relayEpAddrByNodeKey capping bookkeeping at one relay address per peer (endpoint-go, peermap-go).
Where “direct” versus “relay” is actually decided
When someone runs tailscale status and asks why a peer shows a relay code instead of an address, the answer is computed in one small function: endpoint.populatePeerStatus in endpoint.go. Read it and the semantics of the status line stop being folklore (endpoint-go):
ps.Relayis always set to the region code derived fromderpAddr. Every disco capable peer has this; it means “this is their DERP home,” not “traffic is relayed right now.”CurAddr, the direct address in status, is set only whenaddrForSendLockedreturns a valid UDP address and no DERP address, meaning the direct path is currently trusted.- Since the Peer Relays feature (2025), a third case exists: if the chosen address carries a VNI, status reports it as
PeerRelayinstead ofCurAddr(endpoint-go).
So “relayed” in status is not a stored flag anywhere. It is the live output of the same per packet decision function the data path uses: no trusted bestAddr right now means sends are going to DERP, and status shows only the relay. The status line is a window into addrForSendLocked.
A reading order that works
The package is about 17,700 lines including tests (roughly 11,000 without them, counted on the 2026-08-10 checkout), so read it with a goal. This sequence front loads the concepts each later file assumes:
magicsock.go: package comment,Connstruct, thenSendandreceiveIP. You now know the boundary with wireguard-go and the packet classification on receive (magicsock-go).endpoint.go: theendpointstruct comment,addrForSendLocked,send, thenhandlePongConnLockedandbetterAddr, then the constants block back inmagicsock.go(trustUDPAddrDuration,heartbeatInterval,goodEnoughLatency,sessionActiveTimeout). Those four functions come to roughly 300 lines between them, and they are the entire path election (endpoint-go, magicsock-go).derp.go:derpWriteChanForRegion,runDerpReader,processDERPReadResult, and the DERP magic IP encoding (derp-go).peermap.go: small, and it explains everyc.peerMap.endpointFor...lookup you saw earlier (peermap-go).relaymanager.go: read the type comment andrunLoopchannel list first; the state maps make sense once you accept the single goroutine ownership rule (relaymanager-go).- The
discopackage:disco.gois one file of message marshaling (the package holds only it, a fuzzer, a pcap helper, and tests), ten minutes, and wire captures become readable (disco-go).
A concrete lab to cement it: bring up two machines, lab-vm-1 behind a deliberately strict NAT and cloud-1 with an open firewall, run traffic, and watch the client logs for the disco lines while toggling the firewall. You will see the exact sequence this guide traced: DERP first, call-me-maybe, ping storms, a now using promotion, and, if you break the direct path, a quiet fall back to the relay within one trust window.
Cross references
- Module 01 for the WireGuard model magicsock wraps: one peer, one endpoint, and why the protocol itself never renegotiates paths.
- Module 02 for how the netmap that seeds
endpointStateandderpAddrreaches the client. - Module 03 for the operator level view of STUN, DERP, and Peer Relays that this module grounds in source.
- Module 11 for turning this reading into diagnosis: netcheck output, status fields, and log lines in practice.
- Module 12 for the wider repo map around wgengine and where magicsock sits in the build.