// Package control implements the VPN-internal peer control protocol. // Peers exchange Ping packets over UDP on the VPN control port to maintain // liveness and discover external endpoints for direct connection attempts. package control import ( "encoding/binary" "fmt" "net/netip" ) const ( version = 1 Size = 51 // 1 version + 8 PingTS + 6 SrcV4 + 18 SrcV6 + 18 Dst ) // Ping is the single control packet type exchanged between VPN peers. // // In each peer pair, the peer with the lower VPN IP is the client: it sets // PingTS and sends pings on a timer. The server echoes PingTS back in its // response, allowing the client to compute RTT = now - PingTS. // // Both client and server populate SrcV4, SrcV6, and Dst on every packet so // endpoint information flows in both directions. // // Dst is the recipient's external endpoint as observed by the sender from the // WireGuard handshake source. Zero if the sender has not observed a handshake // from the recipient. type Ping struct { PingTS int64 // Client ping send time in nanoseconds. SrcV4 netip.AddrPort // Sender's discovered IPv4 address and port. SrcV6 netip.AddrPort // Sender's discovered IPv6 address and port. Dst netip.AddrPort } // Marshal encodes p into buf (which must be at least Size bytes) and returns // buf[:Size]. Taking the buffer lets callers reuse one across sends; every // field is written unconditionally so a reused buffer needs no pre-zeroing. func (p Ping) Marshal(buf []byte) []byte { _ = buf[Size-1] // Panic if buffer is too small. buf[0] = version binary.BigEndian.PutUint64(buf[1:9], uint64(p.PingTS)) // SrcV4. if p.SrcV4.IsValid() { a4 := p.SrcV4.Addr().As4() copy(buf[9:13], a4[:]) binary.BigEndian.PutUint16(buf[13:15], p.SrcV4.Port()) } else { clear(buf[9:15]) } // SrcV6. a16 := p.SrcV6.Addr().As16() copy(buf[15:31], a16[:]) binary.BigEndian.PutUint16(buf[31:33], p.SrcV6.Port()) // Dst. a16 = p.Dst.Addr().As16() copy(buf[33:49], a16[:]) binary.BigEndian.PutUint16(buf[49:51], p.Dst.Port()) return buf[:Size] } // Unmarshal decodes a Ping from a fixed-size 51-byte array. func Unmarshal(buf [Size]byte) (Ping, error) { if buf[0] != version { return Ping{}, fmt.Errorf("unknown ping version %d", buf[0]) } p := Ping{ PingTS: int64(binary.BigEndian.Uint64(buf[1:9])), } addr := netip.AddrFrom4([4]byte(buf[9:13])) if !addr.IsUnspecified() && addr.Is4() { p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[13:15])) } addr = netip.AddrFrom16([16]byte(buf[15:31])).Unmap() if !addr.IsUnspecified() && addr.Is6() { p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[31:33])) } addr = netip.AddrFrom16([16]byte(buf[33:49])).Unmap() if !addr.IsUnspecified() { p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[49:51])) } return p, nil }