diff --git a/go.mod b/go.mod index ad3add6..3d15604 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,17 @@ require ( ) require ( + github.com/google/go-cmp v0.6.0 // indirect + github.com/josharian/native v1.1.0 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/mdlayher/genetlink v1.3.2 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/vishvananda/netlink v1.3.1 // indirect + github.com/vishvananda/netns v0.0.5 // indirect golang.org/x/net v0.44.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/text v0.29.0 // indirect + golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect ) diff --git a/go.sum b/go.sum index 0f444e0..19af95f 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,34 @@ git.crumpington.com/lib/go v0.9.1 h1:xLBzcgiZRB6Ky3Ce9hKE+Ko0YbkA4USF4eJk5i5RJF4= git.crumpington.com/lib/go v0.9.1/go.mod h1:5nnfjdnUnj/FHhakaliKQKsKeSkUb0GEUKF3PqRgUXg= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= diff --git a/peer/bitset.go b/peer/bitset.go deleted file mode 100644 index 8d03b50..0000000 --- a/peer/bitset.go +++ /dev/null @@ -1,21 +0,0 @@ -package peer - -const bitSetSize = 512 // Multiple of 64. - -type bitSet [bitSetSize / 64]uint64 - -func (bs *bitSet) Set(i int) { - bs[i/64] |= 1 << (i % 64) -} - -func (bs *bitSet) Clear(i int) { - bs[i/64] &= ^(1 << (i % 64)) -} - -func (bs *bitSet) ClearAll() { - clear(bs[:]) -} - -func (bs *bitSet) Get(i int) bool { - return bs[i/64]&(1<<(i%64)) != 0 -} diff --git a/peer/bitset_test.go b/peer/bitset_test.go deleted file mode 100644 index 01ae82b..0000000 --- a/peer/bitset_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package peer - -import ( - "math/rand" - "testing" -) - -func TestBitSet(t *testing.T) { - state := make([]bool, bitSetSize) - for i := range state { - state[i] = rand.Float32() > 0.5 - } - - bs := bitSet{} - - for i := range state { - if state[i] { - bs.Set(i) - } - } - - for i := range state { - if bs.Get(i) != state[i] { - t.Fatal(i, state[i], bs.Get(i)) - } - } - - for i := range state { - if rand.Float32() > 0.5 { - state[i] = false - bs.Clear(i) - } - } - - for i := range state { - if bs.Get(i) != state[i] { - t.Fatal(i, state[i], bs.Get(i)) - } - } - - bs.ClearAll() - - for i := range state { - if bs.Get(i) { - t.Fatal(i, bs.Get(i)) - } - } -} diff --git a/peer/cipher-control.go b/peer/cipher-control.go deleted file mode 100644 index 178ff97..0000000 --- a/peer/cipher-control.go +++ /dev/null @@ -1,26 +0,0 @@ -package peer - -import "golang.org/x/crypto/nacl/box" - -type controlCipher struct { - sharedKey [32]byte -} - -func newControlCipher(privKey, pubKey []byte) *controlCipher { - shared := [32]byte{} - box.Precompute(&shared, (*[32]byte)(pubKey), (*[32]byte)(privKey)) - return &controlCipher{shared} -} - -func (cc *controlCipher) Encrypt(h Header, data, out []byte) []byte { - const s = controlHeaderSize - out = out[:s+controlCipherOverhead+len(data)] - h.Marshal(out[:s]) - box.SealAfterPrecomputation(out[s:s], data, (*[24]byte)(out[:s]), &cc.sharedKey) - return out -} - -func (cc *controlCipher) Decrypt(encrypted, out []byte) (data []byte, ok bool) { - const s = controlHeaderSize - return box.OpenAfterPrecomputation(out[:0], encrypted[s:], (*[24]byte)(encrypted[:s]), &cc.sharedKey) -} diff --git a/peer/cipher-control_test.go b/peer/cipher-control_test.go deleted file mode 100644 index abeb8d5..0000000 --- a/peer/cipher-control_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package peer - -import ( - "bytes" - "crypto/rand" - "reflect" - "testing" - - "golang.org/x/crypto/nacl/box" -) - -func newControlCipherForTesting() (c1, c2 *controlCipher) { - pubKey1, privKey1, err := box.GenerateKey(rand.Reader) - if err != nil { - panic(err) - } - - pubKey2, privKey2, err := box.GenerateKey(rand.Reader) - if err != nil { - panic(err) - } - - return newControlCipher(privKey1[:], pubKey2[:]), - newControlCipher(privKey2[:], pubKey1[:]) -} - -func TestControlCipher(t *testing.T) { - c1, c2 := newControlCipherForTesting() - - maxSizePlaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead) - rand.Read(maxSizePlaintext) - - testCases := [][]byte{ - make([]byte, 0), - {1}, - {255}, - {1, 2, 3, 4, 5}, - []byte("Hello world"), - maxSizePlaintext, - } - - for _, plaintext := range testCases { - h1 := Header{ - StreamID: controlStreamID, - Counter: 235153, - SourceIP: 4, - DestIP: 88, - } - - encrypted := make([]byte, bufferSize) - - encrypted = c1.Encrypt(h1, plaintext, encrypted) - - h2 := Header{} - h2.Parse(encrypted) - if !reflect.DeepEqual(h1, h2) { - t.Fatal(h1, h2) - } - - decrypted, ok := c2.Decrypt(encrypted, make([]byte, bufferSize)) - if !ok { - t.Fatal(ok) - } - - if !bytes.Equal(decrypted, plaintext) { - t.Fatal("not equal") - } - } -} - -func TestControlCipher_ShortCiphertext(t *testing.T) { - c1, _ := newControlCipherForTesting() - shortText := make([]byte, controlHeaderSize+controlCipherOverhead-1) - rand.Read(shortText) - _, ok := c1.Decrypt(shortText, make([]byte, bufferSize)) - if ok { - t.Fatal(ok) - } -} - -func BenchmarkControlCipher_Encrypt(b *testing.B) { - c1, _ := newControlCipherForTesting() - h1 := Header{ - Counter: 235153, - SourceIP: 4, - DestIP: 88, - } - - plaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead) - rand.Read(plaintext) - - encrypted := make([]byte, bufferSize) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - encrypted = c1.Encrypt(h1, plaintext, encrypted) - } -} - -func BenchmarkControlCipher_Decrypt(b *testing.B) { - c1, c2 := newControlCipherForTesting() - - h1 := Header{ - Counter: 235153, - SourceIP: 4, - DestIP: 88, - } - - plaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead) - rand.Read(plaintext) - - encrypted := make([]byte, bufferSize) - - encrypted = c1.Encrypt(h1, plaintext, encrypted) - - decrypted := make([]byte, bufferSize) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - decrypted, _ = c2.Decrypt(encrypted, decrypted) - } -} diff --git a/peer/cipher-data.go b/peer/cipher-data.go deleted file mode 100644 index 5ce8555..0000000 --- a/peer/cipher-data.go +++ /dev/null @@ -1,61 +0,0 @@ -package peer - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "log" -) - -type dataCipher struct { - key [32]byte - aead cipher.AEAD -} - -func newDataCipher() *dataCipher { - key := [32]byte{} - if _, err := rand.Read(key[:]); err != nil { - log.Fatalf("Failed to read random data: %v", err) - } - return newDataCipherFromKey(key) -} - -func newDataCipherFromKey(key [32]byte) *dataCipher { - block, err := aes.NewCipher(key[:]) - if err != nil { - log.Fatalf("Failed to create new cipher: %v", err) - } - - aead, err := cipher.NewGCM(block) - if err != nil { - log.Fatalf("Failed to create new GCM: %v", err) - } - - return &dataCipher{key: key, aead: aead} -} - -func (sc *dataCipher) Key() [32]byte { - return sc.key -} - -func (sc *dataCipher) Encrypt(h Header, data, out []byte) []byte { - const s = dataHeaderSize - out = out[:s+dataCipherOverhead+len(data)] - h.Marshal(out[:s]) - sc.aead.Seal(out[s:s], out[:s], data, nil) - return out -} - -func (sc *dataCipher) Decrypt(encrypted, out []byte) (data []byte, ok bool) { - const s = dataHeaderSize - if len(encrypted) < s+dataCipherOverhead { - ok = false - return - } - - var err error - - data, err = sc.aead.Open(out[:0], encrypted[:s], encrypted[s:], nil) - ok = err == nil - return -} diff --git a/peer/cipher-data_test.go b/peer/cipher-data_test.go deleted file mode 100644 index 4a388f8..0000000 --- a/peer/cipher-data_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package peer - -import ( - "bytes" - "crypto/rand" - mrand "math/rand/v2" - "reflect" - "testing" -) - -func TestDataCipher(t *testing.T) { - maxSizePlaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead) - rand.Read(maxSizePlaintext) - - testCases := [][]byte{ - make([]byte, 0), - {1}, - {255}, - {1, 2, 3, 4, 5}, - []byte("Hello world"), - maxSizePlaintext, - } - - for _, plaintext := range testCases { - h1 := Header{ - StreamID: dataStreamID, - Counter: 235153, - SourceIP: 4, - DestIP: 88, - } - - encrypted := make([]byte, bufferSize) - - dc1 := newDataCipher() - encrypted = dc1.Encrypt(h1, plaintext, encrypted) - h2 := Header{} - h2.Parse(encrypted) - - dc2 := newDataCipherFromKey(dc1.Key()) - - decrypted, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize)) - if !ok { - t.Fatal(ok) - } - - if !bytes.Equal(plaintext, decrypted) { - t.Fatal("not equal") - } - - if !reflect.DeepEqual(h1, h2) { - t.Fatalf("%v != %v", h1, h2) - } - } -} - -func TestDataCipher_ModifyCiphertext(t *testing.T) { - maxSizePlaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead) - rand.Read(maxSizePlaintext) - - testCases := [][]byte{ - make([]byte, 0), - {1}, - {255}, - {1, 2, 3, 4, 5}, - []byte("Hello world"), - maxSizePlaintext, - } - - for _, plaintext := range testCases { - h1 := Header{ - Counter: 235153, - SourceIP: 4, - DestIP: 88, - } - - encrypted := make([]byte, bufferSize) - - dc1 := newDataCipher() - encrypted = dc1.Encrypt(h1, plaintext, encrypted) - encrypted[mrand.IntN(len(encrypted))]++ - - dc2 := newDataCipherFromKey(dc1.Key()) - - _, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize)) - if ok { - t.Fatal(ok) - } - } -} - -func TestDataCipher_ShortCiphertext(t *testing.T) { - dc1 := newDataCipher() - shortText := make([]byte, dataHeaderSize+dataCipherOverhead-1) - rand.Read(shortText) - _, ok := dc1.Decrypt(shortText, make([]byte, bufferSize)) - if ok { - t.Fatal(ok) - } -} - -func BenchmarkDataCipher_Encrypt(b *testing.B) { - h1 := Header{ - Counter: 235153, - SourceIP: 4, - DestIP: 88, - } - - plaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead) - rand.Read(plaintext) - - encrypted := make([]byte, bufferSize) - - dc1 := newDataCipher() - b.ResetTimer() - for i := 0; i < b.N; i++ { - encrypted = dc1.Encrypt(h1, plaintext, encrypted) - } -} - -func BenchmarkDataCipher_Decrypt(b *testing.B) { - h1 := Header{ - Counter: 235153, - SourceIP: 4, - DestIP: 88, - } - - plaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead) - rand.Read(plaintext) - - encrypted := make([]byte, bufferSize) - - dc1 := newDataCipher() - encrypted = dc1.Encrypt(h1, plaintext, encrypted) - - decrypted := make([]byte, bufferSize) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - decrypted, _ = dc1.Decrypt(encrypted, decrypted) - } -} diff --git a/peer/connreader.go b/peer/connreader.go deleted file mode 100644 index 5427227..0000000 --- a/peer/connreader.go +++ /dev/null @@ -1,46 +0,0 @@ -package peer - -import ( - "log" - "net" - "net/netip" -) - -type ConnReader struct { - Globals - conn *net.UDPConn - buf []byte -} - -func NewConnReader(g Globals, conn *net.UDPConn) *ConnReader { - return &ConnReader{ - Globals: g, - conn: conn, - buf: make([]byte, bufferSize), - } -} - -func (r *ConnReader) Run() { - for { - r.handleNextPacket() - } -} - -func (r *ConnReader) handleNextPacket() { - buf := r.buf[:bufferSize] - n, remoteAddr, err := r.conn.ReadFromUDPAddrPort(buf) - if err != nil { - log.Fatalf("Failed to read from UDP port: %v", err) - } - - if n < headerSize { - return - } - - remoteAddr = netip.AddrPortFrom(remoteAddr.Addr().Unmap(), remoteAddr.Port()) - - buf = buf[:n] - h := parseHeader(buf) - - r.RemotePeers[h.SourceIP].Load().HandlePacket(h, remoteAddr, buf) -} diff --git a/peer/controlmessage.go b/peer/controlmessage.go deleted file mode 100644 index f327291..0000000 --- a/peer/controlmessage.go +++ /dev/null @@ -1,64 +0,0 @@ -package peer - -import ( - "net/netip" - "vppn/m" -) - -// ---------------------------------------------------------------------------- - -type controlMsg[T any] struct { - SrcIP byte - SrcAddr netip.AddrPort - Packet T -} - -func parseControlMsg(srcIP byte, srcAddr netip.AddrPort, buf []byte) (any, error) { - switch buf[0] { - - case packetTypeInit: - packet, err := parsePacketInit(buf) - return controlMsg[packetInit]{ - SrcIP: srcIP, - SrcAddr: srcAddr, - Packet: packet, - }, err - - case packetTypeSyn: - packet, err := parsePacketSyn(buf) - return controlMsg[packetSyn]{ - SrcIP: srcIP, - SrcAddr: srcAddr, - Packet: packet, - }, err - - case packetTypeAck: - packet, err := parsePacketAck(buf) - return controlMsg[packetAck]{ - SrcIP: srcIP, - SrcAddr: srcAddr, - Packet: packet, - }, err - - case packetTypeProbe: - packet, err := parsePacketProbe(buf) - return controlMsg[packetProbe]{ - SrcIP: srcIP, - SrcAddr: srcAddr, - Packet: packet, - }, err - - default: - return nil, errUnknownPacketType - } -} - -// ---------------------------------------------------------------------------- - -type peerUpdateMsg struct { - Peer *m.Peer -} - -// ---------------------------------------------------------------------------- - -type pingTimerMsg struct{} diff --git a/peer/crypto.go b/peer/crypto.go index a533e6d..b68bfd4 100644 --- a/peer/crypto.go +++ b/peer/crypto.go @@ -1,30 +1,15 @@ package peer import ( - "crypto/rand" "log" - "golang.org/x/crypto/nacl/box" - "golang.org/x/crypto/nacl/sign" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -type cryptoKeys struct { - PubKey []byte - PrivKey []byte - PubSignKey []byte - PrivSignKey []byte -} - -func generateKeys() cryptoKeys { - pubKey, privKey, err := box.GenerateKey(rand.Reader) +func generateWGKey() wgtypes.Key { + key, err := wgtypes.GeneratePrivateKey() if err != nil { - log.Fatalf("Failed to generate encryption keys: %v", err) + log.Fatalf("Failed to generate WireGuard private key: %v", err) } - - pubSignKey, privSignKey, err := sign.GenerateKey(rand.Reader) - if err != nil { - log.Fatalf("Failed to generate signing keys: %v", err) - } - - return cryptoKeys{pubKey[:], privKey[:], pubSignKey[:], privSignKey[:]} + return key } diff --git a/peer/dupcheck.go b/peer/dupcheck.go deleted file mode 100644 index 2394b15..0000000 --- a/peer/dupcheck.go +++ /dev/null @@ -1,76 +0,0 @@ -package peer - -type dupCheck struct { - bitSet - head int - tail int - headCounter uint64 - tailCounter uint64 // Also next expected counter value. -} - -func newDupCheck(headCounter uint64) *dupCheck { - return &dupCheck{ - headCounter: headCounter, - tailCounter: headCounter + 1, - tail: 1, - } -} - -func (dc *dupCheck) IsDup(counter uint64) bool { - - // Before head => it's late, say it's a dup. - if counter < dc.headCounter { - return true - } - - // It's within the counter bounds. - if counter < dc.tailCounter { - index := (int(counter-dc.headCounter) + dc.head) % bitSetSize - if dc.Get(index) { - return true - } - - dc.Set(index) - return false - } - - // It's more than 1 beyond the tail. - delta := counter - dc.tailCounter - - // Full clear. - if delta >= bitSetSize-1 { - dc.ClearAll() - dc.Set(0) - - dc.tail = 1 - dc.head = 2 - dc.tailCounter = counter + 1 - dc.headCounter = dc.tailCounter - bitSetSize + 1 - - return false - } - - // Clear if necessary. - for range delta { - dc.put(false) - } - - dc.put(true) - return false -} - -func (dc *dupCheck) put(set bool) { - if set { - dc.Set(dc.tail) - } else { - dc.Clear(dc.tail) - } - - dc.tail = (dc.tail + 1) % bitSetSize - dc.tailCounter++ - - if dc.head == dc.tail { - dc.head = (dc.head + 1) % bitSetSize - dc.headCounter++ - } -} diff --git a/peer/dupcheck_test.go b/peer/dupcheck_test.go deleted file mode 100644 index 2b50d74..0000000 --- a/peer/dupcheck_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package peer - -import ( - "testing" -) - -func TestDupCheck(t *testing.T) { - dc := newDupCheck(0) - - for i := range bitSetSize { - if dc.IsDup(uint64(i)) { - t.Fatal("!") - } - } - - type TestCase struct { - Counter uint64 - Dup bool - } - - testCases := []TestCase{ - {511, true}, - {0, true}, - {1, true}, - {2, true}, - {3, true}, - {63, true}, - {256, true}, - {510, true}, - {511, true}, - {512, false}, - {0, true}, - {512, true}, - {513, false}, - {517, false}, - {512, true}, - {513, true}, - {514, false}, - {515, false}, - {516, false}, - {517, true}, - {2512, false}, - {2512, true}, - {2001, true}, - {2002, false}, - {2002, true}, - {4000, false}, - {4000 - 511, true}, // Too old. - {4000 - 510, false}, // Just in the window. - } - - for i, tc := range testCases { - if ok := dc.IsDup(tc.Counter); ok != tc.Dup { - t.Fatal(i, ok, tc) - } - } -} diff --git a/peer/errors.go b/peer/errors.go deleted file mode 100644 index 5ab1df8..0000000 --- a/peer/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package peer - -import "errors" - -var ( - errMalformedPacket = errors.New("malformed packet") - errUnknownPacketType = errors.New("unknown packet type") -) diff --git a/peer/files.go b/peer/files.go index 6e6afe5..7e18880 100644 --- a/peer/files.go +++ b/peer/files.go @@ -11,14 +11,7 @@ import ( type LocalConfig struct { LocalPeerIP byte Network []byte - PubKey []byte - PrivKey []byte - PubSignKey []byte - PrivSignKey []byte -} - -type startupCount struct { - Count uint16 + WGPrivKey string } func configDir(netName string) string { @@ -41,10 +34,6 @@ func peerStatePath(netName string) string { return filepath.Join(configDir(netName), "state.json") } -func startupCountPath(netName string) string { - return filepath.Join(configDir(netName), "startup_count.json") -} - func statusSocketPath(netName string) string { return filepath.Join(configDir(netName), "status.sock") } @@ -106,10 +95,3 @@ func loadNetworkState(netName string) (ps m.NetworkState, err error) { return ps, loadJson(peerStatePath(netName), &ps) } -func loadStartupCount(netName string) (c startupCount, err error) { - return c, loadJson(startupCountPath(netName), &c) -} - -func storeStartupCount(netName string, c startupCount) error { - return storeJson(c, startupCountPath(netName)) -} diff --git a/peer/globals.go b/peer/globals.go index 861a319..52d4105 100644 --- a/peer/globals.go +++ b/peer/globals.go @@ -1,28 +1,15 @@ package peer import ( - "io" "net" "net/netip" - "sync" - "sync/atomic" "time" + + "golang.zx2c4.com/wireguard/wgctrl" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) const ( - version = 1 - - bufferSize = 8192 // Enough for data packets and encryption buffers. - - if_mtu = 1200 - if_queue_len = 2048 - - controlCipherOverhead = 16 - dataCipherOverhead = 16 - signingOverhead = 64 - - pingInterval = 8 * time.Second - timeoutInterval = 30 * time.Second broadcastInterval = 16 * time.Second broadcastErrorTimeoutInterval = 8 * time.Second ) @@ -31,79 +18,22 @@ var multicastAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom( netip.AddrFrom4([4]byte{224, 0, 0, 157}), 4560)) -// ---------------------------------------------------------------------------- - type Globals struct { LocalConfig // Embed, immutable. - // The number of startups - StartupCount uint16 + // WireGuard private key, client, and device name. Immutable after init. + WGPrivKey wgtypes.Key + WGClient *wgctrl.Client + WGDevName string // Local public address (if available). Immutable. - LocalAddr netip.AddrPort - - // True if local public address is valid. Immutable. + LocalAddr netip.AddrPort LocalAddrValid bool - - // All remote peers by VPN IP. - RemotePeers [256]*atomic.Pointer[Remote] - - // Discovered public addresses. - PubAddrs *pubAddrStore - - // Attempts to ensure that we have a relay available. - RelayHandler *relayHandler - - // Send UDP - Global function to write UDP packets. - SendUDP func(b []byte, addr netip.AddrPort) (n int, err error) - - // Global TUN interface. - IFace io.ReadWriteCloser - - // For trace ID. - NewTraceID func() uint64 } -func NewGlobals( - localConfig LocalConfig, - startupCount startupCount, - localAddr netip.AddrPort, - conn *net.UDPConn, - iface io.ReadWriteCloser, -) (g Globals) { +func NewGlobals(localConfig LocalConfig, localAddr netip.AddrPort) (g Globals) { g.LocalConfig = localConfig - g.StartupCount = startupCount.Count - g.LocalAddr = localAddr g.LocalAddrValid = localAddr.IsValid() - - g.PubAddrs = newPubAddrStore(localAddr) - - g.RelayHandler = newRelayHandler() - - // Use a lock here avoids starvation, at least on my Linux machine. - sendLock := sync.Mutex{} - g.SendUDP = func(b []byte, addr netip.AddrPort) (int, error) { - sendLock.Lock() - n, err := conn.WriteToUDPAddrPort(b, addr) - sendLock.Unlock() - return n, err - } - - g.IFace = iface - - traceID := (uint64(g.StartupCount) << 48) + 1 - g.NewTraceID = func() uint64 { - return atomic.AddUint64(&traceID, 1) - } - - for i := range g.RemotePeers { - g.RemotePeers[i] = &atomic.Pointer[Remote]{} - } - - for i := range g.RemotePeers { - g.RemotePeers[i].Store(newRemote(g, byte(i))) - } - return g } diff --git a/peer/header.go b/peer/header.go deleted file mode 100644 index 887c4dd..0000000 --- a/peer/header.go +++ /dev/null @@ -1,47 +0,0 @@ -package peer - -import "unsafe" - -// ---------------------------------------------------------------------------- - -const ( - headerSize = 12 - controlHeaderSize = 24 - dataHeaderSize = 12 - - dataStreamID = 1 - controlStreamID = 2 -) - -type Header struct { - Version byte - StreamID byte - SourceIP byte - DestIP byte - Counter uint64 // Init with time.Now().Unix << 30 to ensure monotonic. -} - -func parseHeader(b []byte) (h Header) { - h.Version = b[0] - h.StreamID = b[1] - h.SourceIP = b[2] - h.DestIP = b[3] - h.Counter = *(*uint64)(unsafe.Pointer(&b[4])) - return h -} - -func (h *Header) Parse(b []byte) { - h.Version = b[0] - h.StreamID = b[1] - h.SourceIP = b[2] - h.DestIP = b[3] - h.Counter = *(*uint64)(unsafe.Pointer(&b[4])) -} - -func (h *Header) Marshal(buf []byte) { - buf[0] = h.Version - buf[1] = h.StreamID - buf[2] = h.SourceIP - buf[3] = h.DestIP - *(*uint64)(unsafe.Pointer(&buf[4])) = h.Counter -} diff --git a/peer/header_test.go b/peer/header_test.go deleted file mode 100644 index f644a36..0000000 --- a/peer/header_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package peer - -import "testing" - -func TestHeaderMarshalParse(t *testing.T) { - nIn := Header{ - StreamID: 23, - Counter: 3212, - SourceIP: 34, - DestIP: 200, - } - - buf := make([]byte, headerSize) - nIn.Marshal(buf) - - nOut := Header{} - nOut.Parse(buf) - if nIn != nOut { - t.Fatal(nIn, nOut) - } -} diff --git a/peer/hubpoller.go b/peer/hubpoller.go index a0d79d0..9293c9b 100644 --- a/peer/hubpoller.go +++ b/peer/hubpoller.go @@ -5,21 +5,26 @@ import ( "io" "log" "net/http" + "net/netip" "net/url" "time" "vppn/m" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) type HubPoller struct { Globals - client *http.Client - req *http.Request - versions [256]int64 - netName string + holePunch *HolePunch + client *http.Client + req *http.Request + versions [256]int64 + netName string } func NewHubPoller( g Globals, + hp *HolePunch, netName, hubURL, apiKey string, @@ -40,10 +45,11 @@ func NewHubPoller( req.SetBasicAuth("", apiKey) return &HubPoller{ - Globals: g, - client: client, - req: req, - netName: netName, + Globals: g, + holePunch: hp, + client: client, + req: req, + netName: netName, }, nil } @@ -98,14 +104,45 @@ func (hp *HubPoller) applyNetworkState(state m.NetworkState) { if i == int(hp.LocalPeerIP) { continue } - if peer != nil && peer.Version == hp.versions[i] { continue } - - hp.RemotePeers[i].Load().HandlePeerUpdate(peerUpdateMsg{Peer: state.Peers[i]}) + hp.applyPeerConfig(peer) if peer != nil { hp.versions[i] = peer.Version } } } + +func (hp *HubPoller) applyPeerConfig(peer *m.Peer) { + if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen { + return + } + if len(peer.PublicIP1) == 0 || peer.Port1 == 0 { + return + } + + pubKey, err := wgtypes.NewKey(peer.WGPubKey) + if err != nil { + hp.logf("Invalid WG key for peer %d: %v", peer.PeerIP, err) + return + } + + ip, ok := netip.AddrFromSlice(peer.PublicIP1) + if !ok { + hp.logf("Invalid public IP for peer %d", peer.PeerIP) + return + } + endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port1) + + if peer.Relay { + if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil { + hp.logf("Failed to update relay config: %v", err) + } + return + } + + if hp.holePunch != nil { + hp.holePunch.OnEndpointLearned(peer.PeerIP, pubKey, endpoint, true) + } +} diff --git a/peer/ifreader.go b/peer/ifreader.go deleted file mode 100644 index ebade54..0000000 --- a/peer/ifreader.go +++ /dev/null @@ -1,73 +0,0 @@ -package peer - -import ( - "log" -) - -type IFReader struct { - Globals -} - -func NewIFReader(g Globals) *IFReader { - return &IFReader{Globals: g} -} - -func (r *IFReader) Run() { - packet := make([]byte, bufferSize) - for { - r.handleNextPacket(packet) - } -} - -func (r *IFReader) handleNextPacket(packet []byte) { - packet = r.readNextPacket(packet) - remoteIP, ok := r.parsePacket(packet) - if !ok { - return - } - r.RemotePeers[remoteIP].Load().SendDataTo(packet) -} - -func (r *IFReader) readNextPacket(buf []byte) []byte { - n, err := r.IFace.Read(buf[:cap(buf)]) - if err != nil { - log.Fatalf("Failed to read from interface: %v", err) - } - - return buf[:n] -} - -// parsePacket returns the VPN ip for the packet, and a boolean indicating -// success. -func (r *IFReader) parsePacket(buf []byte) (byte, bool) { - n := len(buf) - if n == 0 { - return 0, false - } - - version := buf[0] >> 4 - - switch version { - case 4: - if n < 20 { - r.logf("Short IPv4 packet: %d", len(buf)) - return 0, false - } - return buf[19], true - - case 6: - if len(buf) < 40 { - r.logf("Short IPv6 packet: %d", len(buf)) - return 0, false - } - return buf[39], true - - default: - r.logf("Invalid IP packet version: %v", version) - return 0, false - } -} - -func (*IFReader) logf(s string, args ...any) { - log.Printf("[IFReader] "+s, args...) -} diff --git a/peer/ifreader_test.go b/peer/ifreader_test.go deleted file mode 100644 index 92ec5ac..0000000 --- a/peer/ifreader_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package peer - -/* -func TestIFReader_IPv4(t *testing.T) { - p1, p2, _ := NewPeersForTesting() - - pkt := make([]byte, 1234) - pkt[0] = 4 << 4 - pkt[19] = 2 // IP. - - p1.IFace.UserWrite(pkt) - p1.IFReader.handleNextPacket(newBuf()) - - packets := p2.Conn.Packets() - if len(packets) != 1 { - t.Fatal(packets) - } -} - -func TestIFReader_IPv6(t *testing.T) { - p1, p2, _ := NewPeersForTesting() - - pkt := make([]byte, 1234) - pkt[0] = 6 << 4 - pkt[39] = 2 // IP. - - p1.IFace.UserWrite(pkt) - p1.IFReader.handleNextPacket(newBuf()) - - packets := p2.Conn.Packets() - if len(packets) != 1 { - t.Fatal(packets) - } -} - -func TestIFReader_parsePacket_emptyPacket(t *testing.T) { - r := NewIFReader(nil, nil) - pkt := make([]byte, 0) - if ip, ok := r.parsePacket(pkt); ok { - t.Fatal(ip, ok) - } -} - -func TestIFReader_parsePacket_invalidIPVersion(t *testing.T) { - r := NewIFReader(nil, nil) - - for i := byte(1); i < 16; i++ { - if i == 4 || i == 6 { - continue - } - pkt := make([]byte, 1234) - pkt[0] = i << 4 - - if ip, ok := r.parsePacket(pkt); ok { - t.Fatal(i, ip, ok) - } - } -} - -func TestIFReader_parsePacket_shortIPv4(t *testing.T) { - r := NewIFReader(nil, nil) - - pkt := make([]byte, 19) - pkt[0] = 4 << 4 - - if ip, ok := r.parsePacket(pkt); ok { - t.Fatal(ip, ok) - } -} - -func TestIFReader_parsePacket_shortIPv6(t *testing.T) { - r := NewIFReader(nil, nil) - - pkt := make([]byte, 39) - pkt[0] = 6 << 4 - - if ip, ok := r.parsePacket(pkt); ok { - t.Fatal(ip, ok) - } -} -*/ diff --git a/peer/interface.go b/peer/interface.go deleted file mode 100644 index 0022392..0000000 --- a/peer/interface.go +++ /dev/null @@ -1,137 +0,0 @@ -package peer - -import ( - "fmt" - "io" - "net" - "os" - "syscall" - - "golang.org/x/sys/unix" -) - -func openInterface(network []byte, localIP byte, name string) (io.ReadWriteCloser, error) { - if len(network) != 4 { - return nil, fmt.Errorf("expected network to be 4 bytes, got %d", len(network)) - } - ip := net.IPv4(network[0], network[1], network[2], localIP) - - ////////////////////////// - // Create TUN Interface // - ////////////////////////// - - tunFD, err := syscall.Open("/dev/net/tun", syscall.O_RDWR|unix.O_CLOEXEC, 0600) - if err != nil { - return nil, fmt.Errorf("failed to open TUN device: %w", err) - } - - // New interface request. - req, err := unix.NewIfreq(name) - if err != nil { - return nil, fmt.Errorf("failed to create new TUN interface request: %w", err) - } - - // Flags: - // - // IFF_NO_PI => don't add packet info data to packets sent to the interface. - // IFF_TUN => create a TUN device handling IP packets. - req.SetUint16(unix.IFF_NO_PI | unix.IFF_TUN) - - err = unix.IoctlIfreq(tunFD, unix.TUNSETIFF, req) - if err != nil { - return nil, fmt.Errorf("failed to set TUN device settings: %w", err) - } - - // Name may not be exactly the same? - name = req.Name() - - ///////////// - // Set MTU // - ///////////// - - // We need a socket file descriptor to set other options for some reason. - sockFD, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP) - if err != nil { - return nil, fmt.Errorf("failed to open socket: %w", err) - } - defer unix.Close(sockFD) - - req, err = unix.NewIfreq(name) - if err != nil { - return nil, fmt.Errorf("failed to create MTU interface request: %w", err) - } - - req.SetUint32(if_mtu) - if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFMTU, req); err != nil { - return nil, fmt.Errorf("failed to set interface MTU: %w", err) - } - - ////////////////////// - // Set Queue Length // - ////////////////////// - - req, err = unix.NewIfreq(name) - if err != nil { - return nil, fmt.Errorf("failed to create IP interface request: %w", err) - } - - req.SetUint16(if_queue_len) - if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFTXQLEN, req); err != nil { - return nil, fmt.Errorf("failed to set interface queue length: %w", err) - } - - ///////////////////// - // Set IP and Mask // - ///////////////////// - - req, err = unix.NewIfreq(name) - if err != nil { - return nil, fmt.Errorf("failed to create IP interface request: %w", err) - } - - if err := req.SetInet4Addr(ip.To4()); err != nil { - return nil, fmt.Errorf("failed to set interface request IP: %w", err) - } - - if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFADDR, req); err != nil { - return nil, fmt.Errorf("failed to set interface IP: %w", err) - } - - // SET MASK - must happen after setting address. - req, err = unix.NewIfreq(name) - if err != nil { - return nil, fmt.Errorf("failed to create mask interface request: %w", err) - } - - if err := req.SetInet4Addr(net.IPv4(255, 255, 255, 0).To4()); err != nil { - return nil, fmt.Errorf("failed to set interface request mask: %w", err) - } - - if err := unix.IoctlIfreq(sockFD, unix.SIOCSIFNETMASK, req); err != nil { - return nil, fmt.Errorf("failed to set interface mask: %w", err) - } - - //////////////////////// - // Bring Interface Up // - //////////////////////// - - req, err = unix.NewIfreq(name) - if err != nil { - return nil, fmt.Errorf("failed to create up interface request: %w", err) - } - - // Get current flags. - if err = unix.IoctlIfreq(sockFD, unix.SIOCGIFFLAGS, req); err != nil { - return nil, fmt.Errorf("failed to get interface flags: %w", err) - } - - flags := req.Uint16() | unix.IFF_UP | unix.IFF_RUNNING - - // Set UP flag / broadcast flags. - req.SetUint16(flags) - if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFFLAGS, req); err != nil { - return nil, fmt.Errorf("failed to set interface up: %w", err) - } - - return os.NewFile(uintptr(tunFD), "tun"), nil -} diff --git a/peer/main_test.go b/peer/main_test.go deleted file mode 100644 index c759212..0000000 --- a/peer/main_test.go +++ /dev/null @@ -1,5 +0,0 @@ -package peer - -func newBuf() []byte { - return make([]byte, bufferSize) -} diff --git a/peer/mcreader.go b/peer/mcreader.go index e29bab6..88707ff 100644 --- a/peer/mcreader.go +++ b/peer/mcreader.go @@ -1,47 +1,66 @@ package peer import ( + "encoding/binary" + "fmt" "log" "net" + "net/netip" "time" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -func RunMCReader(g Globals) { +func RunMCReader(g Globals, hp *HolePunch, netName string) { for { - runMCReaderInner(g) + if err := runMCReaderInner(g, hp, netName); err != nil { + log.Printf("[MCReader] %v", err) + } time.Sleep(broadcastErrorTimeoutInterval) } } -func runMCReaderInner(g Globals) { - var ( - buf = make([]byte, bufferSize) - logf = func(s string, args ...any) { - log.Printf("[MCReader] "+s, args...) - } - ) - +func runMCReaderInner(g Globals, hp *HolePunch, netName string) error { conn, err := net.ListenMulticastUDP("udp", nil, multicastAddr) if err != nil { - logf("Failed to bind to multicast address: %v", err) + return fmt.Errorf("bind: %w", err) + } + defer conn.Close() + + buf := make([]byte, 64) + for { + conn.SetReadDeadline(time.Now().Add(32 * time.Second)) + n, src, err := conn.ReadFromUDPAddrPort(buf) + if err != nil { + return fmt.Errorf("read: %w", err) + } + if n != beaconLen { + continue + } + handleBeacon(g, hp, netName, buf[:n], src) + } +} + +func handleBeacon(g Globals, hp *HolePunch, netName string, beacon []byte, src netip.AddrPort) { + peerIPByte := beacon[0] + if peerIPByte == g.LocalPeerIP { return } - for { - conn.SetReadDeadline(time.Now().Add(32 * time.Second)) - n, remoteAddr, err := conn.ReadFromUDPAddrPort(buf[:bufferSize]) - if err != nil { - logf("Failed to read from UDP port): %v", err) + pubKey, err := wgtypes.NewKey(beacon[1:33]) + if err != nil { + return + } + + // Skip relay peers: probing would replace their /24 AllowedIPs with empty. + if state, err := loadNetworkState(netName); err == nil { + if p := state.Peers[peerIPByte]; p != nil && p.Relay { return } - - buf = buf[:n] - h, ok := headerFromLocalDiscoveryPacket(buf) - if !ok { - logf("Failed to open discovery packet?") - continue - } - - g.RemotePeers[h.SourceIP].Load().HandleLocalDiscoveryPacket(h, remoteAddr, buf) } + + wgPort := binary.BigEndian.Uint16(beacon[33:35]) + endpoint := netip.AddrPortFrom(src.Addr().Unmap(), wgPort) + + hp.OnEndpointLearned(peerIPByte, pubKey, endpoint, false) } diff --git a/peer/mcreader_test.go b/peer/mcreader_test.go deleted file mode 100644 index 60feb44..0000000 --- a/peer/mcreader_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package peer - -/* -type mcMockConn struct { - packets chan []byte -} - -func newMCMockConn() *mcMockConn { - return &mcMockConn{make(chan []byte, 32)} -} - -func (c *mcMockConn) WriteToUDP(in []byte, addr *net.UDPAddr) (int, error) { - c.packets <- bytes.Clone(in) - return len(in), nil -} - -func (c *mcMockConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) { - buf := <-c.packets - b = b[:len(buf)] - copy(b, buf) - return len(b), netip.AddrPort{}, nil -} - -func TestMCReader(t *testing.T) { - keys := generateKeys() - super := &mockControlMsgHandler{} - conn := newMCMockConn() - - peers := [256]*atomic.Pointer[RemotePeer]{} - peer := &RemotePeer{ - IP: 1, - Up: true, - PubSignKey: keys.PubSignKey, - } - peers[1] = &atomic.Pointer[RemotePeer]{} - peers[1].Store(peer) - - w := newMCWriter(conn, 1, keys.PrivSignKey) - r := newMCReader(conn, super, peers) - - w.SendLocalDiscovery() - r.handleNextPacket() - - if len(super.Messages) != 1 { - t.Fatal(super.Messages) - } - msg, ok := super.Messages[0].(controlMsg[PacketLocalDiscovery]) - if !ok || msg.SrcIP != 1 { - t.Fatal(ok, msg) - } -} - -func TestMCReader_noHeader(t *testing.T) { - keys := generateKeys() - super := &mockControlMsgHandler{} - conn := newMCMockConn() - - peers := [256]*atomic.Pointer[RemotePeer]{} - peer := &RemotePeer{ - IP: 1, - Up: true, - PubSignKey: keys.PubSignKey, - } - peers[1] = &atomic.Pointer[RemotePeer]{} - peers[1].Store(peer) - - r := newMCReader(conn, super, peers) - conn.WriteToUDP([]byte("0123546789"), nil) - r.handleNextPacket() - - if len(super.Messages) != 0 { - t.Fatal(super.Messages) - } -} - -func TestMCReader_noPeer(t *testing.T) { - keys := generateKeys() - super := &mockControlMsgHandler{} - conn := newMCMockConn() - - peers := [256]*atomic.Pointer[RemotePeer]{} - peer := &RemotePeer{ - IP: 1, - Up: true, - PubSignKey: keys.PubSignKey, - } - peers[1] = &atomic.Pointer[RemotePeer]{} - peers[2] = &atomic.Pointer[RemotePeer]{} - peers[1].Store(peer) - - w := newMCWriter(conn, 2, keys.PrivSignKey) - r := newMCReader(conn, super, peers) - - w.SendLocalDiscovery() - r.handleNextPacket() - - if len(super.Messages) != 0 { - t.Fatal(super.Messages) - } -} - -func TestMCReader_badSignature(t *testing.T) { - keys := generateKeys() - super := &mockControlMsgHandler{} - conn := newMCMockConn() - - peers := [256]*atomic.Pointer[RemotePeer]{} - peer := &RemotePeer{ - IP: 1, - Up: true, - PubSignKey: keys.PubSignKey, - } - peers[1] = &atomic.Pointer[RemotePeer]{} - peers[1].Store(peer) - - w := newMCWriter(conn, 1, keys.PrivSignKey) - w.SendLocalDiscovery() - - // Break signing. - packet := <-conn.packets - packet[0]++ - conn.packets <- packet - - r := newMCReader(conn, super, peers) - - r.handleNextPacket() - - if len(super.Messages) != 0 { - t.Fatal(super.Messages) - } -} -*/ diff --git a/peer/mcwriter.go b/peer/mcwriter.go index 5430aac..b039866 100644 --- a/peer/mcwriter.go +++ b/peer/mcwriter.go @@ -1,54 +1,43 @@ package peer import ( + "encoding/binary" + "fmt" "log" "net" "time" - - "golang.org/x/crypto/nacl/sign" ) -func createLocalDiscoveryPacket(localIP byte, signingKey []byte) []byte { - h := Header{ - SourceIP: localIP, - DestIP: 255, - } - buf := make([]byte, headerSize) - h.Marshal(buf) - out := make([]byte, headerSize+signingOverhead) - return sign.Sign(out[:0], buf, (*[64]byte)(signingKey)) -} - -func headerFromLocalDiscoveryPacket(pkt []byte) (h Header, ok bool) { - if len(pkt) != headerSize+signingOverhead { - return - } - - h.Parse(pkt[signingOverhead:]) - ok = true - return -} - -func verifyLocalDiscoveryPacket(pkt, buf []byte, pubSignKey []byte) bool { - _, ok := sign.Open(buf[:0], pkt, (*[32]byte)(pubSignKey)) - return ok -} - -// ---------------------------------------------------------------------------- - -func RunMCWriter(localIP byte, signingKey []byte) { - discoveryPacket := createLocalDiscoveryPacket(localIP, signingKey) +const beaconLen = 35 // 1 VPN IP byte + 32 WG pubkey + 2 WG listen port +func RunMCWriter(g Globals) { conn, err := net.ListenMulticastUDP("udp", nil, multicastAddr) if err != nil { - log.Fatalf("[MCWriter] Failed to bind to multicast address: %v", err) + log.Fatalf("[MCWriter] bind: %v", err) } for range time.Tick(broadcastInterval) { - log.Printf("[MCWriter] Broadcasting on %v...", multicastAddr) - _, err := conn.WriteToUDP(discoveryPacket, multicastAddr) + beacon, err := buildBeacon(g) if err != nil { - log.Printf("[MCWriter] Failed to write multicast: %v", err) + log.Printf("[MCWriter] build beacon: %v", err) + continue + } + log.Printf("[MCWriter] Broadcasting on %v...", multicastAddr) + if _, err := conn.WriteToUDP(beacon, multicastAddr); err != nil { + log.Printf("[MCWriter] write: %v", err) } } } + +func buildBeacon(g Globals) ([]byte, error) { + dev, err := g.WGClient.Device(g.WGDevName) + if err != nil { + return nil, fmt.Errorf("get WG device: %w", err) + } + beacon := make([]byte, beaconLen) + beacon[0] = g.LocalPeerIP + pubKey := g.WGPrivKey.PublicKey() + copy(beacon[1:33], pubKey[:]) + binary.BigEndian.PutUint16(beacon[33:35], uint16(dev.ListenPort)) + return beacon, nil +} diff --git a/peer/mcwriter_test.go b/peer/mcwriter_test.go deleted file mode 100644 index 74411f4..0000000 --- a/peer/mcwriter_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package peer - -/* -// ---------------------------------------------------------------------------- - -// Testing that we can create and verify a local discovery packet. -func TestVerifyLocalDiscoveryPacket_valid(t *testing.T) { - keys := generateKeys() - - created := createLocalDiscoveryPacket(55, keys.PrivSignKey) - - header, ok := headerFromLocalDiscoveryPacket(created) - if !ok { - t.Fatal(ok) - } - if header.SourceIP != 55 || header.DestIP != 255 { - t.Fatal(header) - } - - if !verifyLocalDiscoveryPacket(created, make([]byte, 1024), keys.PubSignKey) { - t.Fatal("Not valid") - } -} - -// Testing that we don't try to parse short packets. -func TestVerifyLocalDiscoveryPacket_tooShort(t *testing.T) { - keys := generateKeys() - - created := createLocalDiscoveryPacket(55, keys.PrivSignKey) - - _, ok := headerFromLocalDiscoveryPacket(created[:len(created)-1]) - if ok { - t.Fatal(ok) - } -} - -// Testing that modifying a packet makes it invalid. -func TestVerifyLocalDiscoveryPacket_invalid(t *testing.T) { - keys := generateKeys() - - created := createLocalDiscoveryPacket(55, keys.PrivSignKey) - buf := make([]byte, 1024) - for i := range created { - modified := bytes.Clone(created) - modified[i]++ - if verifyLocalDiscoveryPacket(modified, buf, keys.PubSignKey) { - t.Fatal("Verification should have failed.") - } - } -} - -// ---------------------------------------------------------------------------- - -type testUDPWriter struct { - written [][]byte -} - -func (w *testUDPWriter) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) { - w.written = append(w.written, bytes.Clone(b)) - return len(b), nil -} - -func (w *testUDPWriter) Written() [][]byte { - out := w.written - w.written = [][]byte{} - return out -} - -// ---------------------------------------------------------------------------- - -// Testing that the mcWriter sends local discovery packets as expected. -func TestMCWriter_SendLocalDiscovery(t *testing.T) { - keys := generateKeys() - writer := &testUDPWriter{} - - mcw := newMCWriter(writer, 42, keys.PrivSignKey) - mcw.SendLocalDiscovery() - - out := writer.Written() - if len(out) != 1 { - t.Fatal(out) - } - - pkt := out[0] - - header, ok := headerFromLocalDiscoveryPacket(pkt) - if !ok { - t.Fatal(ok) - } - if header.SourceIP != 42 || header.DestIP != 255 { - t.Fatal(header) - } - - if !verifyLocalDiscoveryPacket(pkt, make([]byte, 1024), keys.PubSignKey) { - t.Fatal("Verification should succeed.") - } -} -*/ diff --git a/peer/mock-iface_test.go b/peer/mock-iface_test.go deleted file mode 100644 index ffef5d9..0000000 --- a/peer/mock-iface_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package peer - -import "bytes" - -type TestIFace struct { - out *bytes.Buffer // Toward the network. - in *bytes.Buffer // From the network -} - -func NewTestIFace() *TestIFace { - return &TestIFace{ - out: &bytes.Buffer{}, - in: &bytes.Buffer{}, - } -} - -func (iface *TestIFace) Write(b []byte) (int, error) { - return iface.in.Write(b) -} - -func (iface *TestIFace) Read(b []byte) (int, error) { - return iface.out.Read(b) -} - -func (iface *TestIFace) UserWrite(b []byte) (int, error) { - return iface.out.Write(b) -} - -func (iface *TestIFace) UserRead(b []byte) (int, error) { - return iface.in.Read(b) -} diff --git a/peer/mock-network_test.go b/peer/mock-network_test.go deleted file mode 100644 index 4b5240c..0000000 --- a/peer/mock-network_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package peer - -import ( - "bytes" - "net" - "net/netip" - "sync" -) - -type TestPacket struct { - Addr netip.AddrPort - Data []byte -} - -type TestNetwork struct { - lock sync.Mutex - packets map[netip.AddrPort]chan TestPacket -} - -func NewTestNetwork() *TestNetwork { - return &TestNetwork{packets: map[netip.AddrPort]chan TestPacket{}} -} - -func (n *TestNetwork) NewUDPConn(localAddr netip.AddrPort) *TestUDPConn { - n.lock.Lock() - defer n.lock.Unlock() - if _, ok := n.packets[localAddr]; !ok { - n.packets[localAddr] = make(chan TestPacket, 1024) - } - return &TestUDPConn{ - addr: localAddr, - n: n, - packets: n.packets[localAddr], - } -} - -func (n *TestNetwork) write(b []byte, from, to netip.AddrPort) { - n.lock.Lock() - defer n.lock.Unlock() - if _, ok := n.packets[to]; !ok { - n.packets[to] = make(chan TestPacket, 1024) - } - n.packets[to] <- TestPacket{ - Addr: from, - Data: bytes.Clone(b), - } -} - -type TestUDPConn struct { - addr netip.AddrPort - n *TestNetwork - packets chan TestPacket -} - -func (c *TestUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { - c.n.write(b, c.addr, addr) - return len(b), nil -} - -func (c *TestUDPConn) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) { - return c.WriteToUDPAddrPort(b, addr.AddrPort()) -} - -func (c *TestUDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) { - pkt := <-c.packets - b = b[:len(pkt.Data)] - copy(b, pkt.Data) - return len(b), pkt.Addr, nil -} - -func (c *TestUDPConn) Packets() (out []TestPacket) { - for { - select { - case pkt := <-c.packets: - out = append(out, pkt) - default: - return - } - } -} diff --git a/peer/packets-util.go b/peer/packets-util.go deleted file mode 100644 index 3bdfc67..0000000 --- a/peer/packets-util.go +++ /dev/null @@ -1,182 +0,0 @@ -package peer - -import ( - "net/netip" - "unsafe" -) - -// ---------------------------------------------------------------------------- - -type binWriter struct { - b []byte - i int -} - -func newBinWriter(buf []byte) *binWriter { - buf = buf[:cap(buf)] - return &binWriter{buf, 0} -} - -func (w *binWriter) Bool(b bool) *binWriter { - if b { - return w.Byte(1) - } - return w.Byte(0) -} - -func (w *binWriter) Byte(b byte) *binWriter { - w.b[w.i] = b - w.i++ - return w -} - -func (w *binWriter) SharedKey(key [32]byte) *binWriter { - copy(w.b[w.i:w.i+32], key[:]) - w.i += 32 - return w -} - -func (w *binWriter) Uint16(x uint16) *binWriter { - *(*uint16)(unsafe.Pointer(&w.b[w.i])) = x - w.i += 2 - return w -} - -func (w *binWriter) Uint64(x uint64) *binWriter { - *(*uint64)(unsafe.Pointer(&w.b[w.i])) = x - w.i += 8 - return w -} - -func (w *binWriter) Int64(x int64) *binWriter { - *(*int64)(unsafe.Pointer(&w.b[w.i])) = x - w.i += 8 - return w -} - -func (w *binWriter) AddrPort(addrPort netip.AddrPort) *binWriter { - w.Bool(addrPort.IsValid()) - addr := addrPort.Addr().As16() - copy(w.b[w.i:w.i+16], addr[:]) - w.i += 16 - return w.Uint16(addrPort.Port()) -} - -func (w *binWriter) AddrPort8(l [8]netip.AddrPort) *binWriter { - for _, addrPort := range l { - w.AddrPort(addrPort) - } - return w -} - -func (w *binWriter) Build() []byte { - return w.b[:w.i] -} - -// ---------------------------------------------------------------------------- - -type binReader struct { - b []byte - i int - err error -} - -func newBinReader(buf []byte) *binReader { - return &binReader{b: buf} -} - -func (r *binReader) hasBytes(n int) bool { - if r.err != nil || (len(r.b)-r.i) < n { - r.err = errMalformedPacket - return false - } - return true -} - -func (r *binReader) Bool(b *bool) *binReader { - var bb byte - r.Byte(&bb) - *b = bb != 0 - return r -} - -func (r *binReader) Byte(b *byte) *binReader { - if !r.hasBytes(1) { - return r - } - *b = r.b[r.i] - r.i++ - return r -} - -func (r *binReader) SharedKey(x *[32]byte) *binReader { - if !r.hasBytes(32) { - return r - } - *x = ([32]byte)(r.b[r.i : r.i+32]) - r.i += 32 - return r -} - -func (r *binReader) Uint16(x *uint16) *binReader { - if !r.hasBytes(2) { - return r - } - *x = *(*uint16)(unsafe.Pointer(&r.b[r.i])) - r.i += 2 - return r -} - -func (r *binReader) Uint64(x *uint64) *binReader { - if !r.hasBytes(8) { - return r - } - *x = *(*uint64)(unsafe.Pointer(&r.b[r.i])) - r.i += 8 - return r -} - -func (r *binReader) Int64(x *int64) *binReader { - if !r.hasBytes(8) { - return r - } - *x = *(*int64)(unsafe.Pointer(&r.b[r.i])) - r.i += 8 - return r -} - -func (r *binReader) AddrPort(x *netip.AddrPort) *binReader { - if !r.hasBytes(19) { - return r - } - - var ( - valid bool - port uint16 - ) - - r.Bool(&valid) - addr := netip.AddrFrom16(([16]byte)(r.b[r.i : r.i+16])).Unmap() - r.i += 16 - - r.Uint16(&port) - - if valid { - *x = netip.AddrPortFrom(addr, port) - } else { - *x = netip.AddrPort{} - } - - return r -} - -func (r *binReader) AddrPort8(x *[8]netip.AddrPort) *binReader { - for i := range x { - r.AddrPort(&x[i]) - } - return r -} - -func (r *binReader) Error() error { - return r.err -} diff --git a/peer/packets-util_test.go b/peer/packets-util_test.go deleted file mode 100644 index 6e4a98c..0000000 --- a/peer/packets-util_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package peer - -import ( - "net/netip" - "reflect" - "testing" -) - -func TestBinWriteRead_invalidAddrPort(t *testing.T) { - addr := netip.AddrPort{} - buf := make([]byte, 1024) - buf = newBinWriter(buf). - AddrPort(addr). - Build() - - var addr2 netip.AddrPort - err := newBinReader(buf). - AddrPort(&addr2). - Error() - if err != nil { - t.Fatal(err) - } - - if addr2.IsValid() { - t.Fatal(addr, addr2) - } -} - -func TestBinWriteRead(t *testing.T) { - buf := make([]byte, 1024) - - type Item struct { - Type byte - TraceID uint64 - Addrs [8]netip.AddrPort - DestAddr netip.AddrPort - } - - in := Item{ - 1, - 2, - [8]netip.AddrPort{}, - netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 22), - } - - in.Addrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{0, 1, 2, 3}), 20) - in.Addrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 5}), 22) - in.Addrs[3] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 3}), 23) - in.Addrs[4] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 4}), 24) - in.Addrs[5] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 5}), 25) - in.Addrs[6] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 6}), 26) - in.Addrs[7] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{7, 8, 9, 7}), 27) - - buf = newBinWriter(buf). - Byte(in.Type). - Uint64(in.TraceID). - AddrPort(in.DestAddr). - AddrPort8(in.Addrs). - Build() - - out := Item{} - - err := newBinReader(buf). - Byte(&out.Type). - Uint64(&out.TraceID). - AddrPort(&out.DestAddr). - AddrPort8(&out.Addrs). - Error() - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(in, out) { - t.Fatal(in, out) - } -} diff --git a/peer/packets.go b/peer/packets.go deleted file mode 100644 index b673a4c..0000000 --- a/peer/packets.go +++ /dev/null @@ -1,120 +0,0 @@ -package peer - -import ( - "net/netip" -) - -const ( - packetTypeSyn = 1 - packetTypeInit = 2 - packetTypeAck = 3 - packetTypeProbe = 4 - packetTypeAddrDiscovery = 5 -) - -// ---------------------------------------------------------------------------- - -type packetInit struct { - TraceID uint64 - Direct bool - Version uint64 -} - -func (p packetInit) Marshal(buf []byte) []byte { - return newBinWriter(buf). - Byte(packetTypeInit). - Uint64(p.TraceID). - Bool(p.Direct). - Uint64(p.Version). - Build() -} - -func parsePacketInit(buf []byte) (p packetInit, err error) { - err = newBinReader(buf[1:]). - Uint64(&p.TraceID). - Bool(&p.Direct). - Uint64(&p.Version). - Error() - return -} - -// ---------------------------------------------------------------------------- - -type packetSyn struct { - TraceID uint64 // TraceID to match response w/ request. - SharedKey [32]byte // Our shared key. - Direct bool - PossibleAddrs [8]netip.AddrPort // Possible public addresses of the sender. -} - -func (p packetSyn) Marshal(buf []byte) []byte { - return newBinWriter(buf). - Byte(packetTypeSyn). - Uint64(p.TraceID). - SharedKey(p.SharedKey). - Bool(p.Direct). - AddrPort8(p.PossibleAddrs). - Build() -} - -func parsePacketSyn(buf []byte) (p packetSyn, err error) { - err = newBinReader(buf[1:]). - Uint64(&p.TraceID). - SharedKey(&p.SharedKey). - Bool(&p.Direct). - AddrPort8(&p.PossibleAddrs). - Error() - return -} - -// ---------------------------------------------------------------------------- - -type packetAck struct { - TraceID uint64 - ToAddr netip.AddrPort - PossibleAddrs [8]netip.AddrPort // Possible public addresses of the sender. -} - -func (p packetAck) Marshal(buf []byte) []byte { - return newBinWriter(buf). - Byte(packetTypeAck). - Uint64(p.TraceID). - AddrPort(p.ToAddr). - AddrPort8(p.PossibleAddrs). - Build() -} - -func parsePacketAck(buf []byte) (p packetAck, err error) { - err = newBinReader(buf[1:]). - Uint64(&p.TraceID). - AddrPort(&p.ToAddr). - AddrPort8(&p.PossibleAddrs). - Error() - return -} - -// ---------------------------------------------------------------------------- - -// A probeReqPacket is sent from a client to a server to determine if direct -// UDP communication can be used. -type packetProbe struct { - TraceID uint64 -} - -func (p packetProbe) Marshal(buf []byte) []byte { - return newBinWriter(buf). - Byte(packetTypeProbe). - Uint64(p.TraceID). - Build() -} - -func parsePacketProbe(buf []byte) (p packetProbe, err error) { - err = newBinReader(buf[1:]). - Uint64(&p.TraceID). - Error() - return -} - -// ---------------------------------------------------------------------------- - -type packetLocalDiscovery struct{} diff --git a/peer/packets_test.go b/peer/packets_test.go deleted file mode 100644 index f41817b..0000000 --- a/peer/packets_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package peer - -import ( - "crypto/rand" - "net/netip" - "reflect" - "testing" -) - -func TestSynPacket(t *testing.T) { - p := packetSyn{ - TraceID: 2342342345, - Direct: true, - } - rand.Read(p.SharedKey[:]) - - p.PossibleAddrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 234) - p.PossibleAddrs[1] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 2, 3, 4}), 12399) - p.PossibleAddrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{3, 2, 3, 4}), 60000) - - buf := p.Marshal(newBuf()) - p2, err := parsePacketSyn(buf) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(p, p2) { - t.Fatal(p2) - } -} - -func TestAckPacket(t *testing.T) { - p := packetAck{ - TraceID: 123213, - ToAddr: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 234), - } - - p.PossibleAddrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{8, 2, 3, 4}), 100) - p.PossibleAddrs[1] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 2, 3, 4}), 12399) - p.PossibleAddrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{3, 2, 3, 4}), 60000) - - buf := p.Marshal(newBuf()) - p2, err := parsePacketAck(buf) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(p, p2) { - t.Fatal(p2) - } -} - -func TestProbePacket(t *testing.T) { - p := packetProbe{ - TraceID: 12345, - } - - buf := p.Marshal(newBuf()) - p2, err := parsePacketProbe(buf) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(p, p2) { - t.Fatal(p2) - } -} diff --git a/peer/peer.go b/peer/peer.go index f206141..6362cdf 100644 --- a/peer/peer.go +++ b/peer/peer.go @@ -3,11 +3,8 @@ package peer import ( "bytes" "encoding/json" - "fmt" "io" "log" - "math" - "net" "net/http" "net/netip" "net/url" @@ -15,14 +12,17 @@ import ( "vppn/m" "git.crumpington.com/lib/go/flock" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) type peerMain struct { Globals - ifReader *IFReader - connReader *ConnReader - hubPoller *HubPoller - lockFile *os.File + netName string + holePunch *HolePunch + controlServer *ControlServer + endpointReporter *EndpointReporter // non-nil on relay peers only + hubPoller *HubPoller + lockFile *os.File } func newPeerMain(args mainArgs) *peerMain { @@ -59,89 +59,104 @@ func newPeerMain(args mainArgs) *peerMain { log.Fatalf("Failed to load network state: %v", err) } - startupCount, err := loadStartupCount(args.NetName) + wgPrivKey, err := wgtypes.ParseKey(config.WGPrivKey) if err != nil { - if !os.IsNotExist(err) { - log.Fatalf("Failed to load startup count: %v", err) - } - } - - if startupCount.Count == math.MaxUint16 { - log.Fatalf("Startup counter overflow.") - } - startupCount.Count += 1 - - if err := storeStartupCount(args.NetName, startupCount); err != nil { - log.Fatalf("Failed to write startup count: %v", err) - } - - iface, err := openInterface(config.Network, config.LocalPeerIP, args.NetName) - if err != nil { - log.Fatalf("Failed to open interface: %v", err) + log.Fatalf("Failed to parse WireGuard private key: %v", err) } localPeer := state.Peers[config.LocalPeerIP] + var listenPort int + if localPeer != nil { + listenPort = int(localPeer.Port1) + } - myAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", localPeer.Port)) + vpnIP := netip.AddrFrom4([4]byte{ + config.Network[0], + config.Network[1], + config.Network[2], + config.LocalPeerIP, + }) + + wgClient, err := createWGDevice(args.NetName, wgPrivKey, listenPort, vpnIP, config.Network) if err != nil { - log.Fatalf("Failed to resolve UDP address: %v", err) + log.Fatalf("Failed to create WireGuard device: %v", err) } - logf("Listening on %v...", myAddr) - conn, err := net.ListenUDP("udp", myAddr) + for _, p := range state.Peers { + if p == nil || !p.Relay || p.PeerIP == config.LocalPeerIP { + continue + } + if len(p.WGPubKey) != wgtypes.KeyLen || len(p.PublicIP1) == 0 || p.Port1 == 0 { + continue + } + relayPubKey, err := wgtypes.NewKey(p.WGPubKey) + if err != nil { + logf("Invalid relay WG key: %v", err) + continue + } + relayIP, ok := netip.AddrFromSlice(p.PublicIP1) + if !ok { + continue + } + relayEndpoint := netip.AddrPortFrom(relayIP.Unmap(), p.Port1) + if err := applyBaseConfig(wgClient, args.NetName, relayPubKey, relayEndpoint, config.Network); err != nil { + logf("Failed to apply relay base config: %v", err) + } + break + } + + g := NewGlobals(config, netip.AddrPort{}) + g.WGPrivKey = wgPrivKey + g.WGClient = wgClient + g.WGDevName = args.NetName + + holePunch := NewHolePunch(g) + + controlServer, err := NewControlServer(g, holePunch, args.NetName) if err != nil { - log.Fatalf("Failed to open UDP port: %v", err) + log.Fatalf("Failed to create control server: %v", err) } - conn.SetReadBuffer(1024 * 1024 * 8) - conn.SetWriteBuffer(1024 * 1024 * 8) - - var localAddr netip.AddrPort - ip, localAddrValid := netip.AddrFromSlice(localPeer.PublicIP) - if localAddrValid { - localAddr = netip.AddrPortFrom(ip, localPeer.Port) + var endpointReporter *EndpointReporter + if localPeer != nil && localPeer.Relay { + if err := enableForwarding(args.NetName); err != nil { + log.Fatalf("Failed to enable IP forwarding: %v", err) + } + endpointReporter = NewEndpointReporter(g, controlServer, args.NetName) } - g := NewGlobals(config, startupCount, localAddr, conn, iface) - - hubPoller, err := NewHubPoller(g, args.NetName, args.HubAddress, args.APIKey) + hubPoller, err := NewHubPoller(g, holePunch, args.NetName, args.HubAddress, args.APIKey) if err != nil { log.Fatalf("Failed to create hub poller: %v", err) } - // Start status server. go runStatusServer(g, statusSocketPath(args.NetName)) return &peerMain{ - Globals: g, - ifReader: NewIFReader(g), - connReader: NewConnReader(g, conn), - hubPoller: hubPoller, - lockFile: lockFile, + Globals: g, + netName: args.NetName, + holePunch: holePunch, + controlServer: controlServer, + endpointReporter: endpointReporter, + hubPoller: hubPoller, + lockFile: lockFile, } } func (p *peerMain) Run() { - for i := range p.RemotePeers { - remote := p.RemotePeers[i].Load() - go newRemoteFSM(remote).Run() + go p.controlServer.Run() + if p.endpointReporter != nil { + go p.endpointReporter.Run() } - - go p.ifReader.Run() - go p.connReader.Run() - - if !p.LocalAddrValid { - go RunMCWriter(p.LocalPeerIP, p.PrivSignKey) - go RunMCReader(p.Globals) - } - + go RunMCWriter(p.Globals) + go RunMCReader(p.Globals, p.holePunch, p.netName) go p.hubPoller.Run() - select {} } func initPeerWithHub(args mainArgs) { - keys := generateKeys() + privKey := generateWGKey() + pubKey := privKey.PublicKey() initURL, err := url.Parse(args.HubAddress) if err != nil { @@ -150,8 +165,7 @@ func initPeerWithHub(args mainArgs) { initURL.Path = "/peer/init/" initArgs := m.PeerInitArgs{ - EncPubKey: keys.PubKey, - PubSignKey: keys.PubSignKey, + WGPubKey: pubKey[:], } buf := &bytes.Buffer{} @@ -176,18 +190,23 @@ func initPeerWithHub(args mainArgs) { log.Fatalf("Failed to read response body: %v", err) } + if resp.StatusCode == http.StatusConflict { + log.Fatalf("WireGuard key already registered (HTTP 409). Delete and re-create the peer to re-register.") + } + if resp.StatusCode != http.StatusOK { + log.Fatalf("Hub returned unexpected status %d: %s", resp.StatusCode, data) + } + initResp := m.PeerInitResp{} if err := json.Unmarshal(data, &initResp); err != nil { log.Fatalf("Failed to parse configuration: %v\n%s", err, data) } - config := LocalConfig{} - config.LocalPeerIP = initResp.PeerIP - config.Network = initResp.Network - config.PubKey = keys.PubKey - config.PrivKey = keys.PrivKey - config.PubSignKey = keys.PubSignKey - config.PrivSignKey = keys.PrivSignKey + config := LocalConfig{ + LocalPeerIP: initResp.PeerIP, + Network: initResp.Network, + WGPrivKey: privKey.String(), + } if err := storeNetworkState(args.NetName, initResp.NetworkState); err != nil { log.Fatalf("Failed to store network state: %v", err) diff --git a/peer/pubaddrs.go b/peer/pubaddrs.go deleted file mode 100644 index 7945458..0000000 --- a/peer/pubaddrs.go +++ /dev/null @@ -1,86 +0,0 @@ -package peer - -import ( - "net/netip" - "sort" - "sync" - "time" -) - -type pubAddrStore struct { - lock sync.Mutex - localPub bool - localAddr netip.AddrPort - lastSeen map[netip.AddrPort]time.Time - addrList []netip.AddrPort -} - -func newPubAddrStore(localAddr netip.AddrPort) *pubAddrStore { - return &pubAddrStore{ - localPub: localAddr.IsValid(), - localAddr: localAddr, - lastSeen: map[netip.AddrPort]time.Time{}, - addrList: make([]netip.AddrPort, 0, 32), - } -} - -func (store *pubAddrStore) Store(addr netip.AddrPort) { - if store.localPub { - return - } - - if !addr.IsValid() { - return - } - - if addr.Addr().IsPrivate() { - return - } - - store.lock.Lock() - defer store.lock.Unlock() - - if _, exists := store.lastSeen[addr]; !exists { - store.addrList = append(store.addrList, addr) - } - store.lastSeen[addr] = time.Now() - store.sort() -} - -func (store *pubAddrStore) Get() (addrs [8]netip.AddrPort) { - store.lock.Lock() - defer store.lock.Unlock() - - store.clean() - - if store.localPub { - addrs[0] = store.localAddr - return - } - - copy(addrs[:], store.addrList) - return -} - -func (store *pubAddrStore) clean() { - if store.localPub { - return - } - - for ip, lastSeen := range store.lastSeen { - if time.Since(lastSeen) > timeoutInterval { - delete(store.lastSeen, ip) - } - } - store.addrList = store.addrList[:0] - for ip := range store.lastSeen { - store.addrList = append(store.addrList, ip) - } - store.sort() -} - -func (store *pubAddrStore) sort() { - sort.Slice(store.addrList, func(i, j int) bool { - return store.lastSeen[store.addrList[j]].Before(store.lastSeen[store.addrList[i]]) - }) -} diff --git a/peer/pubaddrs_test.go b/peer/pubaddrs_test.go deleted file mode 100644 index fa47c22..0000000 --- a/peer/pubaddrs_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package peer - -import ( - "net/netip" - "testing" - "time" -) - -func TestPubAddrStore(t *testing.T) { - s := newPubAddrStore(netip.AddrPort{}) - - l := []netip.AddrPort{ - netip.AddrPortFrom(netip.AddrFrom4([4]byte{0, 1, 2, 3}), 20), - netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 2, 3}), 21), - netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 1, 2, 3}), 22), - } - - for i := range l { - s.Store(l[i]) - time.Sleep(time.Millisecond) - } - - s.clean() - - l2 := s.Get() - if l2[0] != l[2] || l2[1] != l[1] || l2[2] != l[0] { - t.Fatal(l, l2) - } -} diff --git a/peer/relayhandler.go b/peer/relayhandler.go deleted file mode 100644 index a8e9b3d..0000000 --- a/peer/relayhandler.go +++ /dev/null @@ -1,54 +0,0 @@ -package peer - -import ( - "log" - "sync" - "sync/atomic" -) - -type relayHandler struct { - lock sync.Mutex - relays map[byte]*Remote - relay atomic.Pointer[Remote] -} - -func newRelayHandler() *relayHandler { - return &relayHandler{ - relays: make(map[byte]*Remote, 256), - } -} - -func (h *relayHandler) Add(r *Remote) { - h.lock.Lock() - defer h.lock.Unlock() - - h.relays[r.RemotePeerIP] = r - - if h.relay.Load() == nil { - log.Printf("Setting Relay: %v", r.conf().Peer.Name) - h.relay.Store(r) - } -} - -func (h *relayHandler) Remove(r *Remote) { - h.lock.Lock() - defer h.lock.Unlock() - - log.Printf("Removing relay %d...", r.RemotePeerIP) - delete(h.relays, r.RemotePeerIP) - - if h.relay.Load() == r { - // Remove current relay. - h.relay.Store(nil) - - // Find new relay. - for _, r := range h.relays { - h.relay.Store(r) - break - } - } -} - -func (h *relayHandler) Load() *Remote { - return h.relay.Load() -} diff --git a/peer/remote.go b/peer/remote.go deleted file mode 100644 index 9f815fa..0000000 --- a/peer/remote.go +++ /dev/null @@ -1,351 +0,0 @@ -package peer - -import ( - "fmt" - "log" - "net/netip" - "strings" - "sync/atomic" - "vppn/m" -) - -// ---------------------------------------------------------------------------- -// The remoteConfig is the shared, immutable configuration for a remote -// peer. It's read and written atomically. See remote.config. -// ---------------------------------------------------------------------------- - -type remoteConfig struct { - Up bool // True if peer is up and we can send data. - Server bool // True if role is server. - Direct bool // True if this is a direct connection. - DirectAddr netip.AddrPort // Remote address if directly connected. - ControlCipher *controlCipher - DataCipher *dataCipher - Peer *m.Peer -} - -// CanRelay returns true if the remote configuration is able to relay packets. -// to other hosts. -func (rc remoteConfig) CanRelay() bool { - return rc.Up && rc.Direct && rc.Peer.Relay -} - -// A Remote represents a remote peer and contains functions for handling -// incoming control, data, and multicast packets, peer udpates, as well as -// sending, forwarding, and relaying packets. -type Remote struct { - Globals - RemotePeerIP byte // Immutable. - - dupCheck *dupCheck - sendCounter uint64 // init to startupCount << 48. Atomic access only. - - // config should be accessed via conf() and updateConf(...) methods. - config atomic.Pointer[remoteConfig] - messages chan any -} - -func newRemote(g Globals, remotePeerIP byte) *Remote { - r := &Remote{ - Globals: g, - RemotePeerIP: remotePeerIP, - dupCheck: newDupCheck(0), - sendCounter: (uint64(g.StartupCount) << 48) + 1, - messages: make(chan any, 8), - } - r.config.Store(&remoteConfig{}) - return r -} - -// ---------------------------------------------------------------------------- - -func (r *Remote) conf() remoteConfig { - return *(r.config.Load()) -} - -func (r *Remote) updateConf(conf remoteConfig) { - old := r.config.Load() - r.config.Store(&conf) - - if !old.CanRelay() && conf.CanRelay() { - r.RelayHandler.Add(r) - } - - if old.CanRelay() && !conf.CanRelay() { - r.RelayHandler.Remove(r) - } -} - -// ---------------------------------------------------------------------------- - -func (r *Remote) sendUDP(b []byte, addr netip.AddrPort) { - if _, err := r.SendUDP(b, addr); err != nil { - r.logf("Failed to send UDP packet: %v", err) - } -} - -// ---------------------------------------------------------------------------- - -func (r *Remote) encryptData(conf remoteConfig, destIP byte, packet []byte) []byte { - h := Header{ - StreamID: dataStreamID, - Counter: atomic.AddUint64(&r.sendCounter, 1), - SourceIP: r.Globals.LocalPeerIP, - DestIP: destIP, - } - return conf.DataCipher.Encrypt(h, packet, packet[len(packet):cap(packet)]) -} - -func (r *Remote) encryptControl(conf remoteConfig, packet []byte) []byte { - h := Header{ - StreamID: controlStreamID, - Counter: atomic.AddUint64(&r.sendCounter, 1), - SourceIP: r.LocalPeerIP, - DestIP: r.RemotePeerIP, - } - return conf.ControlCipher.Encrypt(h, packet, packet[len(packet):cap(packet)]) -} - -func (r *Remote) Status() (RemoteStatus, bool) { - conf := r.conf() - if conf.Peer == nil { - return RemoteStatus{}, false - } - - return RemoteStatus{ - PeerIP: conf.Peer.PeerIP, - Up: conf.Up, - Name: conf.Peer.Name, - PublicIP: conf.Peer.PublicIP, - Port: conf.Peer.Port, - Relay: conf.Peer.Relay, - Server: conf.Server, - Direct: conf.Direct, - DirectAddr: conf.DirectAddr, - }, true -} - -// ---------------------------------------------------------------------------- - -// SendDataTo sends a data packet to the remote, called by the IFReader. -func (r *Remote) SendDataTo(data []byte) { - conf := r.conf() - if !conf.Up { - r.logf("Cannot send: link down") - return - } - - // Direct: - - if conf.Direct { - r.sendUDP(r.encryptData(conf, conf.Peer.PeerIP, data), conf.DirectAddr) - return - } - - // Relayed: - relay := r.RelayHandler.Load() - - if relay == nil { - r.logf("Connot send: no relay") - return - } - - relay.relayData(conf.Peer.PeerIP, r.encryptData(conf, conf.Peer.PeerIP, data)) -} - -func (r *Remote) relayData(toIP byte, enc []byte) { - conf := r.conf() - if !conf.Up || !conf.Direct { - r.logf("Cannot relay: not up or not a direct connection") - return - } - r.sendUDP(r.encryptData(conf, toIP, enc), conf.DirectAddr) -} - -func (r *Remote) sendControl(conf remoteConfig, data []byte) { - // Direct: - - if conf.Direct { - enc := r.encryptControl(conf, data) - r.sendUDP(enc, conf.DirectAddr) - return - } - - // Relayed: - - relay := r.RelayHandler.Load() - - if relay == nil { - r.logf("Connot send: no relay") - return - } - - relay.relayData(conf.Peer.PeerIP, r.encryptControl(conf, data)) -} - -func (r *Remote) sendControlToAddr(buf []byte, addr netip.AddrPort) { - enc := r.encryptControl(r.conf(), buf) - r.sendUDP(enc, addr) -} - -func (r *Remote) forwardPacket(data []byte) { - conf := r.conf() - if !conf.Up || !conf.Direct { - r.logf("Cannot forward to %d: not a direct connection", conf.Peer.PeerIP) - return - } - r.sendUDP(data, conf.DirectAddr) -} - -// ---------------------------------------------------------------------------- - -// HandlePacket is called by the ConnReader to handle an incoming packet. -func (r *Remote) HandlePacket(h Header, srcAddr netip.AddrPort, data []byte) { - switch h.StreamID { - case controlStreamID: - r.handleControlPacket(h, srcAddr, data) - case dataStreamID: - r.handleDataPacket(h, data) - default: - r.logf("Unknown stream ID: %d", h.StreamID) - } -} - -// Handle a control packet. Decrypt, verify, etc. -func (r *Remote) handleControlPacket(h Header, srcAddr netip.AddrPort, data []byte) { - conf := r.conf() - if conf.ControlCipher == nil { - r.logf("No control cipher") - return - } - - dec, ok := conf.ControlCipher.Decrypt(data, data[len(data):cap(data)]) - if !ok { - r.logf("Failed to decrypt control packet") - return - } - - if r.dupCheck.IsDup(h.Counter) { - r.logf("Dropping control packet as duplicate: %d", h.Counter) - return - } - - msg, err := parseControlMsg(h.SourceIP, srcAddr, dec) - if err != nil { - r.logf("Failed to parse control packet: %v", err) - return - } - - select { - case r.messages <- msg: - default: - r.logf("Dropping control message") - } -} - -func (r *Remote) handleDataPacket(h Header, data []byte) { - conf := r.conf() - if conf.DataCipher == nil { - return - } - - dec, ok := conf.DataCipher.Decrypt(data, data[len(data):cap(data)]) - if !ok { - r.logf("Failed to decrypt data packet") - return - } - - if r.dupCheck.IsDup(h.Counter) { - r.logf("Dropping data packet as duplicate: %d", h.Counter) - return - } - - // For local. - if h.DestIP == r.LocalPeerIP { - if _, err := r.IFace.Write(dec); err != nil { - // This could be a malformed packet from a peer, so we don't crash if it - // happens. - r.logf("Failed to write to interface: %v", err) - } - return - } - - // Forward. - dest := r.RemotePeers[h.DestIP].Load() - dest.forwardPacket(dec) -} - -// ---------------------------------------------------------------------------- - -// HandleLocalDiscoveryPacket is called by the MCReader. -func (r *Remote) HandleLocalDiscoveryPacket(h Header, srcAddr netip.AddrPort, data []byte) { - conf := r.conf() - if conf.Peer == nil { - r.logf("No peer for discovery packet.") - return - } - - if conf.Peer.PubSignKey == nil { - r.logf("No signing key for discovery packet.") - return - } - - if !verifyLocalDiscoveryPacket(data, data[len(data):cap(data)], conf.Peer.PubSignKey) { - r.logf("Invalid signature on discovery packet.") - return - } - - msg := controlMsg[packetLocalDiscovery]{ - SrcIP: h.SourceIP, - SrcAddr: srcAddr, - } - - select { - case r.messages <- msg: - default: - r.logf("Dropping discovery message.") - } -} - -// ---------------------------------------------------------------------------- - -// HandlePeerUpdate is called by the HubPoller when it gets a new version of -// the associated peer configuration. -func (r *Remote) HandlePeerUpdate(msg peerUpdateMsg) { - r.messages <- msg -} - -// ---------------------------------------------------------------------------- - -func (s *Remote) logf(format string, args ...any) { - conf := s.conf() - - b := strings.Builder{} - name := "" - if conf.Peer != nil { - name = conf.Peer.Name - } - b.WriteString(fmt.Sprintf("%03d", s.RemotePeerIP)) - - b.WriteString(fmt.Sprintf("%30s: ", name)) - - if conf.Server { - b.WriteString("SERVER | ") - } else { - b.WriteString("CLIENT | ") - } - - if conf.Direct { - b.WriteString("DIRECT | ") - } else { - b.WriteString("RELAYED | ") - } - - if conf.Up { - b.WriteString("UP | ") - } else { - b.WriteString("DOWN | ") - } - - log.Printf(b.String()+format, args...) -} diff --git a/peer/remotefsm.go b/peer/remotefsm.go deleted file mode 100644 index 9f6a442..0000000 --- a/peer/remotefsm.go +++ /dev/null @@ -1,448 +0,0 @@ -package peer - -import ( - "net/netip" - "time" - "vppn/m" -) - -type stateFunc func(msg any) stateFunc - -type sentProbe struct { - SentAt time.Time - Addr netip.AddrPort -} - -type remoteFSM struct { - *Remote - - pingTimer *time.Ticker - lastSeen time.Time - traceID uint64 - probes map[uint64]sentProbe - - buf []byte -} - -func newRemoteFSM(r *Remote) *remoteFSM { - fsm := &remoteFSM{ - Remote: r, - pingTimer: time.NewTicker(timeoutInterval), - probes: map[uint64]sentProbe{}, - buf: make([]byte, bufferSize), - } - fsm.pingTimer.Stop() - return fsm -} - -func (r *remoteFSM) Run() { - go func() { - for range r.pingTimer.C { - r.messages <- pingTimerMsg{} - } - }() - state := r.enterDisconnected() - for msg := range r.messages { - state = state(msg) - } -} - -// ---------------------------------------------------------------------------- - -func (r *remoteFSM) enterDisconnected() stateFunc { - r.updateConf(remoteConfig{}) - return r.stateDisconnected -} - -func (r *remoteFSM) stateDisconnected(iMsg any) stateFunc { - switch msg := iMsg.(type) { - case peerUpdateMsg: - return r.enterPeerUpdating(msg.Peer) - case controlMsg[packetInit]: - r.logf("Unexpected INIT") - case controlMsg[packetSyn]: - r.logf("Unexpected SYN") - case controlMsg[packetAck]: - r.logf("Unexpected ACK") - case controlMsg[packetProbe]: - r.logf("Unexpected probe") - case controlMsg[packetLocalDiscovery]: - // Ignore - case pingTimerMsg: - r.logf("Unexpected ping") - default: - r.logf("Ignoring message: %#v", iMsg) - } - - return r.stateDisconnected -} - -// ---------------------------------------------------------------------------- - -func (r *remoteFSM) enterPeerUpdating(peer *m.Peer) stateFunc { - if peer == nil { - return r.enterDisconnected() - } - - conf := remoteConfig{ - Peer: peer, - ControlCipher: newControlCipher(r.PrivKey, peer.PubKey), - } - r.updateConf(conf) - - if _, isValid := netip.AddrFromSlice(peer.PublicIP); isValid { - if r.LocalAddrValid && r.LocalPeerIP < peer.PeerIP { - return r.enterServer() - } - return r.enterClientInit() - } - - if r.LocalAddrValid || r.LocalPeerIP < peer.PeerIP { - return r.enterServer() - } - - return r.enterClientInit() -} - -// ---------------------------------------------------------------------------- - -func (r *remoteFSM) enterServer() stateFunc { - - conf := r.conf() - conf.Server = true - r.updateConf(conf) - r.logf("==> Server") - - r.pingTimer.Reset(pingInterval) - r.lastSeen = time.Now() - return r.stateServer -} - -func (r *remoteFSM) stateServer(iMsg any) stateFunc { - switch msg := iMsg.(type) { - case peerUpdateMsg: - return r.enterPeerUpdating(msg.Peer) - case controlMsg[packetInit]: - r.stateServer_onInit(msg) - case controlMsg[packetSyn]: - r.stateServer_onSyn(msg) - case controlMsg[packetAck]: - r.logf("Unexpected ACK") - case controlMsg[packetProbe]: - r.stateServer_onProbe(msg) - case controlMsg[packetLocalDiscovery]: - // Ignore - case pingTimerMsg: - r.stateServer_onPingTimer() - default: - r.logf("Unexpected message: %#v", iMsg) - } - - return r.stateServer -} - -func (r *remoteFSM) stateServer_onInit(msg controlMsg[packetInit]) { - conf := r.conf() - conf.Up = false - conf.Direct = msg.Packet.Direct - conf.DirectAddr = msg.SrcAddr - r.updateConf(conf) - - init := packetInit{ - TraceID: msg.Packet.TraceID, - Direct: conf.Direct, - Version: version, - } - - // Reset traceID to force state update on SYN. - r.traceID = 0 - r.sendControl(conf, init.Marshal(r.buf)) -} - -func (r *remoteFSM) stateServer_onSyn(msg controlMsg[packetSyn]) { - r.lastSeen = time.Now() - p := msg.Packet - - conf := r.conf() - - // New trace ID => Update the route configuration. - if p.TraceID != r.traceID { - r.traceID = p.TraceID - - conf.Up = true - conf.Direct = p.Direct - conf.DirectAddr = msg.SrcAddr - - conf.DataCipher = newDataCipherFromKey(p.SharedKey) - - r.updateConf(conf) - r.logf("Got SYN.") - } - - r.sendControl(conf, packetAck{ - TraceID: p.TraceID, - ToAddr: conf.DirectAddr, - PossibleAddrs: r.PubAddrs.Get(), - }.Marshal(r.buf)) - - if p.Direct { - return - } - - // Send probes if not a direct connection. The server sends probes without - // trace IDs unless responding to a client probe. - for _, addr := range msg.Packet.PossibleAddrs { - if !addr.IsValid() { - break - } - r.logf("Probing %v...", addr) - r.sendControlToAddr(packetProbe{}.Marshal(r.buf), addr) - } -} - -func (r *remoteFSM) stateServer_onProbe(msg controlMsg[packetProbe]) { - if !msg.SrcAddr.IsValid() { - return - } - - data := packetProbe{TraceID: msg.Packet.TraceID}.Marshal(r.buf) - r.sendControlToAddr(data, msg.SrcAddr) -} - -func (r *remoteFSM) stateServer_onPingTimer() { - conf := r.conf() - if time.Since(r.lastSeen) > timeoutInterval && conf.Up { - // Reset trace ID to ensure connection goes up on next SYN. - r.traceID = 0 - conf.Up = false - r.updateConf(conf) - r.logf("Timeout.") - } -} - -// ---------------------------------------------------------------------------- - -func (r *remoteFSM) enterClientInit() stateFunc { - conf := r.conf() - ip, ipValid := netip.AddrFromSlice(conf.Peer.PublicIP) - - conf.Up = false - conf.Server = false - conf.Direct = ipValid - conf.DirectAddr = netip.AddrPortFrom(ip, conf.Peer.Port) - conf.DataCipher = newDataCipher() - - r.updateConf(conf) - r.logf("==> ClientInit") - - r.lastSeen = time.Now() - r.pingTimer.Reset(pingInterval) - r.stateClientInit_sendInit() - return r.stateClientInit -} - -func (r *remoteFSM) stateClientInit(iMsg any) stateFunc { - switch msg := iMsg.(type) { - case peerUpdateMsg: - return r.enterPeerUpdating(msg.Peer) - case controlMsg[packetInit]: - return r.stateClientInit_onInit(msg) - case controlMsg[packetSyn]: - r.logf("Unexpected SYN") - case controlMsg[packetAck]: - r.logf("Unexpected ACK") - case controlMsg[packetProbe]: - // Ignore - case controlMsg[packetLocalDiscovery]: - // Ignore - case pingTimerMsg: - return r.stateClientInit_onPing() - default: - r.logf("Unexpected message: %#v", iMsg) - } - - return r.stateClientInit -} - -func (r *remoteFSM) stateClientInit_sendInit() { - conf := r.conf() - r.traceID = r.NewTraceID() - init := packetInit{ - TraceID: r.traceID, - Direct: conf.Direct, - Version: version, - } - r.sendControl(conf, init.Marshal(r.buf)) -} - -func (r *remoteFSM) stateClientInit_onInit(msg controlMsg[packetInit]) stateFunc { - if msg.Packet.TraceID != r.traceID { - r.logf("Invalid trace ID on INIT.") - return r.stateClientInit - } - r.logf("Got INIT version %d.", msg.Packet.Version) - return r.enterClient() -} - -func (r *remoteFSM) stateClientInit_onPing() stateFunc { - if time.Since(r.lastSeen) < timeoutInterval { - r.stateClientInit_sendInit() - return r.stateClientInit - } - - // Direct connect failed. Try indirect. - conf := r.conf() - - if conf.Direct { - conf.Direct = false - r.updateConf(conf) - r.lastSeen = time.Now() - r.stateClientInit_sendInit() - r.logf("Direct connection failed. Attempting indirect connection.") - return r.stateClientInit - } - - // Indirect failed. Re-enter init state. - r.logf("Timeout.") - return r.enterClientInit() -} - -// ---------------------------------------------------------------------------- - -func (r *remoteFSM) enterClient() stateFunc { - conf := r.conf() - clear(r.probes) - - r.traceID = r.NewTraceID() - r.stateClient_sendSyn(conf) - - r.pingTimer.Reset(pingInterval) - r.logf("==> Client") - return r.stateClient -} - -func (r *remoteFSM) stateClient(iMsg any) stateFunc { - switch msg := iMsg.(type) { - case peerUpdateMsg: - return r.enterPeerUpdating(msg.Peer) - case controlMsg[packetAck]: - r.stateClient_onAck(msg) - case controlMsg[packetProbe]: - r.stateClient_onProbe(msg) - case controlMsg[packetLocalDiscovery]: - r.stateClient_onLocalDiscovery(msg) - case pingTimerMsg: - return r.stateClient_onPingTimer() - default: - r.logf("Ignoring message: %v", iMsg) - } - return r.stateClient -} - -func (r *remoteFSM) stateClient_onAck(msg controlMsg[packetAck]) { - if msg.Packet.TraceID != r.traceID { - return - } - - r.lastSeen = time.Now() - - conf := r.conf() - if !conf.Up { - conf.Up = true - r.updateConf(conf) - r.logf("Got ACK.") - } - - if conf.Direct { - r.PubAddrs.Store(msg.Packet.ToAddr) - return - } - - // Relayed. - - r.stateClient_cleanProbes() - - for _, addr := range msg.Packet.PossibleAddrs { - if !addr.IsValid() { - break - } - r.stateClient_sendProbeTo(addr) - } -} - -func (r *remoteFSM) stateClient_cleanProbes() { - for key, sent := range r.probes { - if time.Since(sent.SentAt) > pingInterval { - delete(r.probes, key) - } - } -} - -func (r *remoteFSM) stateClient_sendProbeTo(addr netip.AddrPort) { - probe := packetProbe{TraceID: r.NewTraceID()} - r.probes[probe.TraceID] = sentProbe{ - SentAt: time.Now(), - Addr: addr, - } - r.logf("Probing %v...", addr) - r.sendControlToAddr(probe.Marshal(r.buf), addr) -} - -func (r *remoteFSM) stateClient_onProbe(msg controlMsg[packetProbe]) { - conf := r.conf() - if conf.Direct { - return - } - - r.stateClient_cleanProbes() - - sent, ok := r.probes[msg.Packet.TraceID] - if !ok { - return - } - - conf.Direct = true - conf.DirectAddr = sent.Addr - r.updateConf(conf) - - r.traceID = r.NewTraceID() - r.stateClient_sendSyn(conf) - r.logf("Successful probe to %v.", sent.Addr) -} - -func (r *remoteFSM) stateClient_onLocalDiscovery(msg controlMsg[packetLocalDiscovery]) { - conf := r.conf() - if conf.Direct { - return - } - - // The source port will be the multicast port, so we'll have to - // construct the correct address using the peer's listed port. - addr := netip.AddrPortFrom(msg.SrcAddr.Addr(), conf.Peer.Port) - r.stateClient_sendProbeTo(addr) -} - -func (r *remoteFSM) stateClient_onPingTimer() stateFunc { - conf := r.conf() - - if time.Since(r.lastSeen) > timeoutInterval { - if conf.Up { - r.logf("Timeout.") - } - return r.enterClientInit() - } - - r.stateClient_sendSyn(conf) - return r.stateClient -} - -func (r *remoteFSM) stateClient_sendSyn(conf remoteConfig) { - syn := packetSyn{ - TraceID: r.traceID, - SharedKey: conf.DataCipher.Key(), - Direct: conf.Direct, - PossibleAddrs: r.PubAddrs.Get(), - } - - r.sendControl(conf, syn.Marshal(r.buf)) -} diff --git a/peer/statusserver.go b/peer/statusserver.go index 1eebbfc..77fd48a 100644 --- a/peer/statusserver.go +++ b/peer/statusserver.go @@ -35,32 +35,11 @@ func runStatusServer(g Globals, socketPath string) { report := StatusReport{ LocalPeerIP: g.LocalPeerIP, Network: g.Network, - Remotes: make([]RemoteStatus, 0, 255), } - - relay := g.RelayHandler.Load() - if relay != nil { - if relayStatus, ok := relay.Status(); ok { - report.RelayPeerIP = relayStatus.PeerIP - } - } - - for i := range g.RemotePeers { - remote := g.RemotePeers[i].Load() - status, ok := remote.Status() - if !ok { - continue - } - report.Remotes = append(report.Remotes, status) - } - json.NewEncoder(w).Encode(report) } - server := http.Server{ - Handler: http.HandlerFunc(handler), - } - + server := http.Server{Handler: http.HandlerFunc(handler)} unixListener, err := net.Listen("unix", socketPath) if err != nil { log.Fatalf("Failed to bind to unix socket: %v", err) diff --git a/peer/wginterface/interface.go b/peer/wginterface/interface.go new file mode 100644 index 0000000..f73d609 --- /dev/null +++ b/peer/wginterface/interface.go @@ -0,0 +1,224 @@ +// Package wginterface demonstrates creating and destroying a WireGuard network +// interface using only raw system calls — no netlink library. +// +// Creating a typed interface (kind = "wireguard") requires the NETLINK_ROUTE +// protocol; there is no ioctl path for it. Everything else — assigning an IP +// address and bringing the link up — can be done with the older AF_INET ioctl +// interface, exactly as one would for a TUN device. +// +// The package requires CAP_NET_ADMIN and the wireguard kernel module. +package wginterface + +import ( + "encoding/binary" + "fmt" + "net" + "slices" + + "golang.org/x/sys/unix" +) + +// Create creates a WireGuard interface named name, assigns vpnIP/prefixLen to +// it, and brings it up. +func Create(name string, vpnIP net.IP, prefixLen int) error { + if err := nlNewLink(name); err != nil { + return fmt.Errorf("failed to create wireguard link: %w", err) + } + if err := ioctlSetAddr(name, vpnIP, prefixLen); err != nil { + _ = Delete(name) + return fmt.Errorf("assign address: %w", err) + } + if err := ioctlLinkUp(name); err != nil { + _ = Delete(name) + return fmt.Errorf("link up: %w", err) + } + return nil +} + +// Delete removes the named interface. +func Delete(name string) error { + return nlDelLink(name) +} + +// --------------------------------------------------------------------------- +// Netlink link management +// +// Creating a WireGuard interface requires an RTM_NEWLINK message with a nested +// IFLA_LINKINFO attribute whose IFLA_INFO_KIND is "wireguard". The full +// message layout is: +// +// nlmsghdr (16 bytes) +// ifinfomsg (16 bytes, all zeros for a new link) +// rtattr IFLA_IFNAME → name + \0 +// rtattr IFLA_LINKINFO +// rtattr IFLA_INFO_KIND → "wireguard" + \0 +// +// All multi-byte integers are in native byte order (little-endian on +// x86/arm64). Every attribute is padded to a 4-byte boundary; the len field +// in the header records the unpadded length but the attribute occupies the +// padded size. + +const ( + nlmsgHdrLen = 16 // sizeof(struct nlmsghdr) + sizeofIfInfo = 16 // sizeof(struct ifinfomsg) + + // Attribute types not exposed by the unix package at the level we need. + iflaLinkInfo = 18 // IFLA_LINKINFO — container for link-type attributes + iflaInfoKind = 1 // IFLA_INFO_KIND — link type string, nested inside IFLA_LINKINFO +) + +// nlNewLink creates the wireguard interface using Netlink. +func nlNewLink(name string) error { + // Build innermost attribute first, then wrap outward. + infoKind := nlAttr(iflaInfoKind, cstring("wireguard")) + linkInfo := nlAttr(iflaLinkInfo, infoKind) + ifName := nlAttr(unix.IFLA_IFNAME, cstring(name)) + + // ifinfomsg: all-zero = AF_UNSPEC, no index, no flags (kernel assigns index). + ifInfo := make([]byte, sizeofIfInfo) + + payload := slices.Concat(ifInfo, ifName, linkInfo) + flags := uint16(unix.NLM_F_REQUEST | unix.NLM_F_ACK | unix.NLM_F_CREATE | unix.NLM_F_EXCL) + return nlRoundtrip(unix.RTM_NEWLINK, flags, payload) +} + +func nlDelLink(name string) error { + iface, err := net.InterfaceByName(name) + if err != nil { + return err + } + + // For RTM_DELLINK the kernel identifies the link by ifi_index. ifi_index + // sits at byte offset 4 in the ifinfomsg struct. + ifInfo := make([]byte, sizeofIfInfo) + binary.NativeEndian.PutUint32(ifInfo[4:8], uint32(iface.Index)) + + return nlRoundtrip(unix.RTM_DELLINK, uint16(unix.NLM_F_REQUEST|unix.NLM_F_ACK), ifInfo) +} + +// nlRoundtrip opens a NETLINK_ROUTE socket, sends one request, reads the +// NLMSG_ERROR acknowledgement, and closes the socket. +func nlRoundtrip(msgType uint16, flags uint16, payload []byte) error { + fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_ROUTE) + if err != nil { + return fmt.Errorf("socket: %w", err) + } + defer unix.Close(fd) + + if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil { + return fmt.Errorf("bind: %w", err) + } + + msg := nlMsg(msgType, flags, payload) + if err := unix.Sendto(fd, msg, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil { + return fmt.Errorf("sendto: %w", err) + } + + resp := make([]byte, 4096) + n, _, err := unix.Recvfrom(fd, resp, 0) + if err != nil { + return fmt.Errorf("recvfrom: %w", err) + } + return nlAckErr(resp[:n]) +} + +// nlMsg prepends an nlmsghdr to payload. +func nlMsg(msgType uint16, flags uint16, payload []byte) []byte { + buf := make([]byte, nlmsgHdrLen+len(payload)) + binary.NativeEndian.PutUint32(buf[0:4], uint32(len(buf))) // nlmsg_len + binary.NativeEndian.PutUint16(buf[4:6], msgType) // nlmsg_type + binary.NativeEndian.PutUint16(buf[6:8], flags) // nlmsg_flags + binary.NativeEndian.PutUint32(buf[8:12], 1) // nlmsg_seq + binary.NativeEndian.PutUint32(buf[12:16], 0) // nlmsg_pid (0 = kernel) + copy(buf[nlmsgHdrLen:], payload) + return buf +} + +// nlAckErr parses an NLMSG_ERROR response. The error field is a negated errno +// (0 = success, -EEXIST = interface exists, etc.). +func nlAckErr(resp []byte) error { + if len(resp) < nlmsgHdrLen+4 { + return fmt.Errorf("netlink response too short (%d bytes)", len(resp)) + } + if binary.NativeEndian.Uint16(resp[4:6]) != unix.NLMSG_ERROR { + return fmt.Errorf("unexpected nlmsg_type %d", binary.NativeEndian.Uint16(resp[4:6])) + } + // Error code follows the nlmsghdr; it is a signed int32 holding -errno. + code := int32(binary.NativeEndian.Uint32(resp[nlmsgHdrLen:])) + if code != 0 { + return unix.Errno(-code) + } + return nil +} + +// nlAttr encodes one netlink attribute: [len:u16][type:u16][data][pad to 4 +// bytes]. The len field counts the header + data (before padding); the +// allocation is padded so that the next attribute starts on a 4-byte boundary. +func nlAttr(attrType uint16, data []byte) []byte { + const hdr = 4 + attrLen := hdr + len(data) + padded := (attrLen + 3) &^ 3 + buf := make([]byte, padded) + binary.NativeEndian.PutUint16(buf[0:2], uint16(attrLen)) + binary.NativeEndian.PutUint16(buf[2:4], attrType) + copy(buf[hdr:], data) + return buf +} + +// --------------------------------------------------------------------------- +// ioctl-based address assignment and link-up +// +// These operations could also be done via RTM_NEWADDR / RTM_NEWLINK netlink +// messages, but the AF_INET ioctl interface is simpler. + +func ioctlSetAddr(name string, ip net.IP, prefixLen int) error { + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP) + if err != nil { + return err + } + defer unix.Close(fd) + + req, err := unix.NewIfreq(name) + if err != nil { + return err + } + if err := req.SetInet4Addr(ip.To4()); err != nil { + return err + } + if err := unix.IoctlIfreq(fd, unix.SIOCSIFADDR, req); err != nil { + return err + } + + req, err = unix.NewIfreq(name) + if err != nil { + return err + } + mask := net.CIDRMask(prefixLen, 32) + if err := req.SetInet4Addr([]byte(mask)); err != nil { + return err + } + return unix.IoctlIfreq(fd, unix.SIOCSIFNETMASK, req) +} + +func ioctlLinkUp(name string) error { + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP) + if err != nil { + return err + } + defer unix.Close(fd) + + req, err := unix.NewIfreq(name) + if err != nil { + return err + } + if err := unix.IoctlIfreq(fd, unix.SIOCGIFFLAGS, req); err != nil { + return err + } + req.SetUint16(req.Uint16() | unix.IFF_UP | unix.IFF_RUNNING) + return unix.IoctlIfreq(fd, unix.SIOCSIFFLAGS, req) +} + +// cstring returns b as a null-terminated byte slice. +func cstring(s string) []byte { + return append([]byte(s), 0) +} diff --git a/peer/wginterface/manage.go b/peer/wginterface/manage.go new file mode 100644 index 0000000..bdb1b5b --- /dev/null +++ b/peer/wginterface/manage.go @@ -0,0 +1,165 @@ +package wginterface + +import ( + "fmt" + "net" + "net/netip" + "os" + "time" + + "golang.zx2c4.com/wireguard/wgctrl" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +const ( + // RekeyTimeout is the WireGuard session lifetime before a new handshake + // is initiated. Sessions older than this but younger than SessionTimeout + // remain valid. + RekeyTimeout = 120 * time.Second + + // SessionTimeout is the WireGuard session lifetime after which sessions + // are rejected. A peer with LastHandshakeTime older than this is + // effectively disconnected. + SessionTimeout = 180 * time.Second +) + +var ( + probeKeepalive = 5 * time.Second + zeroKeepalive = time.Duration(0) +) + +// Device wraps a wgctrl client bound to a named WireGuard interface. +type Device struct { + client *wgctrl.Client + name string +} + +// Open attaches to an existing WireGuard interface. +func Open(name string) (*Device, error) { + client, err := wgctrl.New() + if err != nil { + return nil, fmt.Errorf("wgctrl: %w", err) + } + return &Device{client: client, name: name}, nil +} + +// Close releases the underlying wgctrl client. +func (d *Device) Close() error { + return d.client.Close() +} + +// Configure sets the device's private key and UDP listen port. +func (d *Device) Configure(privKey wgtypes.Key, listenPort int) error { + return d.client.ConfigureDevice(d.name, wgtypes.Config{ + PrivateKey: &privKey, + ListenPort: &listenPort, + }) +} + +// Peers returns the current state of all peers on the device. +func (d *Device) Peers() ([]wgtypes.Peer, error) { + dev, err := d.client.Device(d.name) + if err != nil { + return nil, fmt.Errorf("get device %q: %w", d.name, err) + } + return dev.Peers, nil +} + +// Peer returns the current state of a single peer by public key. +func (d *Device) Peer(pubKey wgtypes.Key) (wgtypes.Peer, error) { + peers, err := d.Peers() + if err != nil { + return wgtypes.Peer{}, err + } + for _, p := range peers { + if p.PublicKey == pubKey { + return p, nil + } + } + return wgtypes.Peer{}, fmt.Errorf("peer %v not found in %q", pubKey, d.name) +} + +// SetRelay configures the relay peer with AllowedIPs covering the entire VPN +// network prefix. This is the fallback route for all VPN traffic. +func (d *Device) SetRelay(pubKey wgtypes.Key, endpoint netip.AddrPort, network netip.Prefix) error { + masked := network.Masked() + a4 := masked.Addr().As4() + return d.client.ConfigureDevice(d.name, wgtypes.Config{ + Peers: []wgtypes.PeerConfig{{ + PublicKey: pubKey, + Endpoint: net.UDPAddrFromAddrPort(endpoint), + AllowedIPs: []net.IPNet{{ + IP: net.IP(a4[:]), + Mask: net.CIDRMask(masked.Bits(), 32), + }}, + ReplaceAllowedIPs: true, + }}, + }) +} + +// AddProbe adds a peer with no AllowedIPs and a 5s keepalive. WireGuard will +// attempt handshakes without routing any traffic through this peer yet. +func (d *Device) AddProbe(pubKey wgtypes.Key, endpoint netip.AddrPort) error { + return d.client.ConfigureDevice(d.name, wgtypes.Config{ + Peers: []wgtypes.PeerConfig{{ + PublicKey: pubKey, + Endpoint: net.UDPAddrFromAddrPort(endpoint), + AllowedIPs: []net.IPNet{}, + ReplaceAllowedIPs: true, + PersistentKeepaliveInterval: &probeKeepalive, + }}, + }) +} + +// Promote upgrades a probe entry to a /32 AllowedIPs and removes the probe +// keepalive, causing WireGuard to prefer this peer's direct path over the +// relay's wider route. +func (d *Device) Promote(pubKey wgtypes.Key, vpnIP netip.Addr) error { + a4 := vpnIP.As4() + return d.client.ConfigureDevice(d.name, wgtypes.Config{ + Peers: []wgtypes.PeerConfig{{ + PublicKey: pubKey, + AllowedIPs: []net.IPNet{{ + IP: net.IP(a4[:]), + Mask: net.CIDRMask(32, 32), + }}, + ReplaceAllowedIPs: true, + PersistentKeepaliveInterval: &zeroKeepalive, + }}, + }) +} + +// AddDirect adds a peer with a known endpoint and /32 AllowedIPs in one step, +// for peers with a stable public endpoint reported by the hub. +func (d *Device) AddDirect(pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) error { + a4 := vpnIP.As4() + return d.client.ConfigureDevice(d.name, wgtypes.Config{ + Peers: []wgtypes.PeerConfig{{ + PublicKey: pubKey, + Endpoint: net.UDPAddrFromAddrPort(endpoint), + AllowedIPs: []net.IPNet{{ + IP: net.IP(a4[:]), + Mask: net.CIDRMask(32, 32), + }}, + ReplaceAllowedIPs: true, + PersistentKeepaliveInterval: &zeroKeepalive, + }}, + }) +} + +// RemovePeer removes a peer from the device. +func (d *Device) RemovePeer(pubKey wgtypes.Key) error { + return d.client.ConfigureDevice(d.name, wgtypes.Config{ + Peers: []wgtypes.PeerConfig{{ + PublicKey: pubKey, + Remove: true, + }}, + }) +} + +// EnableForwarding enables IPv4 forwarding on the interface, required for +// relay peers that forward traffic between VPN peers. +func (d *Device) EnableForwarding() error { + path := fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/forwarding", d.name) + return os.WriteFile(path, []byte("1\n"), 0644) +} diff --git a/peer/wginterface/manage_test.go b/peer/wginterface/manage_test.go new file mode 100644 index 0000000..b809e2d --- /dev/null +++ b/peer/wginterface/manage_test.go @@ -0,0 +1,303 @@ +//go:build integration + +package wginterface_test + +import ( + "fmt" + "log" + "net" + "net/netip" + "os" + "strings" + "testing" + "time" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "vppn/peer/wginterface" +) + +const ( + testBasePort = 59100 +) + +func TestMain(m *testing.M) { + if os.Getuid() != 0 { + fmt.Fprintln(os.Stderr, "wginterface integration tests require root; skipping") + os.Exit(0) + } + os.Exit(m.Run()) +} + +type testPeer struct { + Name string + VpnIP netip.Addr + Port int + PrivKey wgtypes.Key + PubKey wgtypes.Key + Dev *wginterface.Device +} + +func (p *testPeer) Endpoint() netip.AddrPort { + return netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), uint16(p.Port)) +} + +func newTestPeer(t *testing.T, name string, vpnIP netip.Addr, port int) *testPeer { + t.Helper() + + privKey, err := wgtypes.GenerateKey() + if err != nil { + t.Fatalf("generate key: %v", err) + } + + a4 := vpnIP.As4() + if err := wginterface.Create(name, net.IP(a4[:]), 24); err != nil { + t.Fatalf("create %s: %v", name, err) + } + t.Cleanup(func() { + if err := wginterface.Delete(name); err != nil { + log.Printf("Failed to delete interface %s: %v", name, err) + } + }) + + dev, err := wginterface.Open(name) + if err != nil { + t.Fatalf("open %s: %v", name, err) + } + t.Cleanup(func() { dev.Close() }) + + if err := dev.Configure(privKey, port); err != nil { + t.Fatalf("configure %s: %v", name, err) + } + + return &testPeer{ + Name: name, + VpnIP: vpnIP, + Port: port, + PrivKey: privKey, + PubKey: privKey.PublicKey(), + Dev: dev, + } +} + +// waitHandshake polls until the named peer has completed a handshake or the timeout elapses. +func waitHandshake(t *testing.T, dev *wginterface.Device, pubKey wgtypes.Key, timeout time.Duration) { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + p, err := dev.Peer(pubKey) + if err != nil { + t.Fatalf("peer lookup: %v", err) + } + if !p.LastHandshakeTime.IsZero() { + return + } + time.Sleep(200 * time.Millisecond) + } + t.Fatalf("no handshake within %v", timeout) +} + +func TestDirectHandshake(t *testing.T) { + p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort) + p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1) + + if err := p1.Dev.AddDirect(p2.PubKey, p2.Endpoint(), p2.VpnIP); err != nil { + t.Fatalf("p1 AddDirect: %v", err) + } + if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil { + t.Fatalf("p2 AddDirect: %v", err) + } + + waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second) + waitHandshake(t, p2.Dev, p1.PubKey, 30*time.Second) +} + +func TestProbeAndPromote(t *testing.T) { + p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort) + p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1) + + // p2 needs a peer entry for p1 so it can respond to the handshake initiation. + if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil { + t.Fatalf("p2 AddDirect: %v", err) + } + + if err := p1.Dev.AddProbe(p2.PubKey, p2.Endpoint()); err != nil { + t.Fatalf("AddProbe: %v", err) + } + waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second) + + if err := p1.Dev.Promote(p2.PubKey, p2.VpnIP); err != nil { + t.Fatalf("Promote: %v", err) + } + + peer, err := p1.Dev.Peer(p2.PubKey) + if err != nil { + t.Fatalf("Peer: %v", err) + } + checkAllowedIP(t, peer, p2.VpnIP, 32) +} + +func TestRelayHandshakes(t *testing.T) { + vpnNetwork := netip.MustParsePrefix("192.168.99.0/24") + + relay := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort) + peer1 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1) + peer2 := newTestPeer(t, "wgtest2", netip.MustParseAddr("192.168.99.3"), testBasePort+2) + + if err := relay.Dev.AddDirect(peer1.PubKey, peer1.Endpoint(), peer1.VpnIP); err != nil { + t.Fatalf("relay AddDirect peer1: %v", err) + } + if err := relay.Dev.AddDirect(peer2.PubKey, peer2.Endpoint(), peer2.VpnIP); err != nil { + t.Fatalf("relay AddDirect peer2: %v", err) + } + if err := peer1.Dev.SetRelay(relay.PubKey, relay.Endpoint(), vpnNetwork); err != nil { + t.Fatalf("peer1 SetRelay: %v", err) + } + if err := peer2.Dev.SetRelay(relay.PubKey, relay.Endpoint(), vpnNetwork); err != nil { + t.Fatalf("peer2 SetRelay: %v", err) + } + + waitHandshake(t, relay.Dev, peer1.PubKey, 30*time.Second) + waitHandshake(t, relay.Dev, peer2.PubKey, 30*time.Second) + waitHandshake(t, peer1.Dev, relay.PubKey, 30*time.Second) + waitHandshake(t, peer2.Dev, relay.PubKey, 30*time.Second) + + // relay has /32 entries for each peer + p, err := relay.Dev.Peer(peer1.PubKey) + if err != nil { + t.Fatalf("relay peer1: %v", err) + } + checkAllowedIP(t, p, peer1.VpnIP, 32) + + p, err = relay.Dev.Peer(peer2.PubKey) + if err != nil { + t.Fatalf("relay peer2: %v", err) + } + checkAllowedIP(t, p, peer2.VpnIP, 32) + + // peers have /24 fallback route via relay + p, err = peer1.Dev.Peer(relay.PubKey) + if err != nil { + t.Fatalf("peer1 relay: %v", err) + } + checkAllowedIP(t, p, vpnNetwork.Masked().Addr(), 24) + + p, err = peer2.Dev.Peer(relay.PubKey) + if err != nil { + t.Fatalf("peer2 relay: %v", err) + } + checkAllowedIP(t, p, vpnNetwork.Masked().Addr(), 24) +} + +func TestRemovePeer(t *testing.T) { + p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort) + p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1) + + if err := p1.Dev.AddDirect(p2.PubKey, p2.Endpoint(), p2.VpnIP); err != nil { + t.Fatalf("AddDirect: %v", err) + } + if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil { + t.Fatalf("AddDirect: %v", err) + } + waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second) + + if err := p1.Dev.RemovePeer(p2.PubKey); err != nil { + t.Fatalf("RemovePeer: %v", err) + } + if _, err := p1.Dev.Peer(p2.PubKey); err == nil { + t.Fatal("expected error after RemovePeer, got nil") + } +} + +func TestEnableForwarding(t *testing.T) { + p := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort) + + if err := p.Dev.EnableForwarding(); err != nil { + t.Fatalf("EnableForwarding: %v", err) + } + + data, err := os.ReadFile(fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/forwarding", p.Name)) + if err != nil { + t.Fatalf("read forwarding: %v", err) + } + if strings.TrimSpace(string(data)) != "1" { + t.Fatalf("expected forwarding=1, got %q", string(data)) + } +} + +func TestPromoteKeepalive(t *testing.T) { + p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort) + p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1) + + if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil { + t.Fatalf("p2 AddDirect: %v", err) + } + if err := p1.Dev.AddProbe(p2.PubKey, p2.Endpoint()); err != nil { + t.Fatalf("AddProbe: %v", err) + } + waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second) + + if err := p1.Dev.Promote(p2.PubKey, p2.VpnIP); err != nil { + t.Fatalf("Promote: %v", err) + } + + peer, err := p1.Dev.Peer(p2.PubKey) + if err != nil { + t.Fatalf("Peer: %v", err) + } + if peer.PersistentKeepaliveInterval != 0 { + t.Fatalf("expected keepalive disabled after promote, got %v", peer.PersistentKeepaliveInterval) + } +} + +func TestPeersCount(t *testing.T) { + relay := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort) + peer1 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1) + peer2 := newTestPeer(t, "wgtest2", netip.MustParseAddr("192.168.99.3"), testBasePort+2) + + if err := relay.Dev.AddDirect(peer1.PubKey, peer1.Endpoint(), peer1.VpnIP); err != nil { + t.Fatalf("AddDirect peer1: %v", err) + } + if err := relay.Dev.AddDirect(peer2.PubKey, peer2.Endpoint(), peer2.VpnIP); err != nil { + t.Fatalf("AddDirect peer2: %v", err) + } + + peers, err := relay.Dev.Peers() + if err != nil { + t.Fatalf("Peers: %v", err) + } + if len(peers) != 2 { + t.Fatalf("expected 2 peers, got %d", len(peers)) + } + + if err := relay.Dev.RemovePeer(peer1.PubKey); err != nil { + t.Fatalf("RemovePeer: %v", err) + } + + peers, err = relay.Dev.Peers() + if err != nil { + t.Fatalf("Peers after remove: %v", err) + } + if len(peers) != 1 { + t.Fatalf("expected 1 peer after remove, got %d", len(peers)) + } + if peers[0].PublicKey != peer2.PubKey { + t.Fatal("wrong peer remained after remove") + } +} + +// checkAllowedIP asserts that a peer has exactly one AllowedIP matching addr/bits. +func checkAllowedIP(t *testing.T, p wgtypes.Peer, addr netip.Addr, bits int) { + t.Helper() + if len(p.AllowedIPs) != 1 { + t.Fatalf("expected 1 AllowedIP, got %d", len(p.AllowedIPs)) + } + ones, _ := p.AllowedIPs[0].Mask.Size() + if ones != bits { + t.Fatalf("expected /%d, got /%d", bits, ones) + } + got := netip.AddrFrom4([4]byte(p.AllowedIPs[0].IP.To4())) + if got != addr { + t.Fatalf("expected AllowedIP %v, got %v", addr, got) + } +}