79 lines
2.7 KiB
Go
79 lines
2.7 KiB
Go
// 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))
|
|
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])
|
|
}
|
|
a16 := p.SrcV6.Addr().As16()
|
|
copy(buf[15:31], a16[:])
|
|
binary.BigEndian.PutUint16(buf[31:33], p.SrcV6.Port())
|
|
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])),
|
|
}
|
|
if addr := netip.AddrFrom4([4]byte(buf[9:13])); !addr.IsUnspecified() {
|
|
p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[13:15]))
|
|
}
|
|
if addr := netip.AddrFrom16([16]byte(buf[15:31])); !addr.IsUnspecified() {
|
|
p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[31:33]))
|
|
}
|
|
if addr := netip.AddrFrom16([16]byte(buf[33:49])).Unmap(); !addr.IsUnspecified() {
|
|
p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[49:51]))
|
|
}
|
|
return p, nil
|
|
}
|