Refactor - now wireguard based. (#7)
This commit is contained in:
76
peer/control/ping.go
Normal file
76
peer/control/ping.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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[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
|
||||
}
|
||||
106
peer/control/ping_test.go
Normal file
106
peer/control/ping_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package control_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"vppn/peer/control"
|
||||
)
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ping control.Ping
|
||||
}{
|
||||
{
|
||||
name: "zero",
|
||||
ping: control.Ping{},
|
||||
},
|
||||
{
|
||||
name: "client ping",
|
||||
ping: control.Ping{
|
||||
PingTS: 1234567890,
|
||||
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
|
||||
Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "server response",
|
||||
ping: control.Ping{
|
||||
PingTS: 1234567890,
|
||||
SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||
Dst: netip.MustParseAddrPort("1.2.3.4:9999"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6 only",
|
||||
ping: control.Ping{
|
||||
PingTS: 999,
|
||||
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
||||
Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dual stack",
|
||||
ping: control.Ping{
|
||||
PingTS: 555,
|
||||
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
|
||||
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
||||
Dst: netip.MustParseAddrPort("5.6.7.8:9999"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no src known",
|
||||
ping: control.Ping{
|
||||
Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf [control.Size]byte
|
||||
tc.ping.Marshal(buf[:])
|
||||
got, err := control.Unmarshal(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if got != tc.ping {
|
||||
t.Fatalf("round-trip mismatch:\n got %+v\n want %+v", got, tc.ping)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalBadVersion(t *testing.T) {
|
||||
var buf [control.Size]byte
|
||||
buf[0] = 99
|
||||
if _, err := control.Unmarshal(buf); err == nil {
|
||||
t.Fatal("expected error for unknown version, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroEncoding(t *testing.T) {
|
||||
var buf [control.Size]byte
|
||||
(control.Ping{}).Marshal(buf[:])
|
||||
for i, b := range buf {
|
||||
if i == 0 {
|
||||
continue // version byte
|
||||
}
|
||||
if b != 0 {
|
||||
t.Fatalf("expected zero encoding at byte %d, got %d", i, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleFor(t *testing.T) {
|
||||
lo := netip.MustParseAddr("10.0.0.1")
|
||||
hi := netip.MustParseAddr("10.0.0.2")
|
||||
|
||||
if control.RoleFor(lo, hi) != control.Client {
|
||||
t.Error("lower IP should be client")
|
||||
}
|
||||
if control.RoleFor(hi, lo) != control.Server {
|
||||
t.Error("higher IP should be server")
|
||||
}
|
||||
}
|
||||
22
peer/control/role.go
Normal file
22
peer/control/role.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package control
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// Role identifies a peer's role in a ping exchange with a specific remote peer.
|
||||
type Role string
|
||||
|
||||
const (
|
||||
// Client initiates pings and measures RTT.
|
||||
Client Role = "CLIENT"
|
||||
// Server responds to pings.
|
||||
Server Role = "SERVER"
|
||||
)
|
||||
|
||||
// RoleFor returns the Role of local relative to remote.
|
||||
// The peer with the lower VPN IP is the client.
|
||||
func RoleFor(local, remote netip.Addr) Role {
|
||||
if local.Compare(remote) < 0 {
|
||||
return Client
|
||||
}
|
||||
return Server
|
||||
}
|
||||
Reference in New Issue
Block a user