From 2522b0615fb3ee97de323f4c296f68e8ccdb07a3 Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Tue, 28 Mar 2023 12:20:07 -0700 Subject: [PATCH] tempfork/device: add a temp fork of golang.zx2c4.com/wireguard/device This will allow us to reuse the AllowedIPs for NAT decisions in a follow on commit. The files `allowedips_*.go` are as-is, `peer.go` only keeps the `Peer` declaration with a single element required for AllowedIPs. Upstream commit https://git.zx2c4.com/wireguard-go/commit/?id=052af4a8072bbbd3bfe7edf46fe3c1b350f71f08 Updates tailscale/corp#8020 Signed-off-by: Maisem Ali --- tempfork/device/LICENSE | 17 ++ tempfork/device/README.md | 3 + tempfork/device/allowedips.go | 294 ++++++++++++++++++++++++ tempfork/device/allowedips_rand_test.go | 141 ++++++++++++ tempfork/device/allowedips_test.go | 247 ++++++++++++++++++++ tempfork/device/peer.go | 14 ++ 6 files changed, 716 insertions(+) create mode 100644 tempfork/device/LICENSE create mode 100644 tempfork/device/README.md create mode 100644 tempfork/device/allowedips.go create mode 100644 tempfork/device/allowedips_rand_test.go create mode 100644 tempfork/device/allowedips_test.go create mode 100644 tempfork/device/peer.go diff --git a/tempfork/device/LICENSE b/tempfork/device/LICENSE new file mode 100644 index 000000000..a3fdf73d5 --- /dev/null +++ b/tempfork/device/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/tempfork/device/README.md b/tempfork/device/README.md new file mode 100644 index 000000000..945e25527 --- /dev/null +++ b/tempfork/device/README.md @@ -0,0 +1,3 @@ +This is a fork of golang.zx2c4.com/wireguard/device that only keeps the bare +minimum data structures required for AllowedIPs. It is meant to be short lived +until we replace it with our version of a routing table. diff --git a/tempfork/device/allowedips.go b/tempfork/device/allowedips.go new file mode 100644 index 000000000..fa46f97c1 --- /dev/null +++ b/tempfork/device/allowedips.go @@ -0,0 +1,294 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package device + +import ( + "container/list" + "encoding/binary" + "errors" + "math/bits" + "net" + "net/netip" + "sync" + "unsafe" +) + +type parentIndirection struct { + parentBit **trieEntry + parentBitType uint8 +} + +type trieEntry struct { + peer *Peer + child [2]*trieEntry + parent parentIndirection + cidr uint8 + bitAtByte uint8 + bitAtShift uint8 + bits []byte + perPeerElem *list.Element +} + +func commonBits(ip1, ip2 []byte) uint8 { + size := len(ip1) + if size == net.IPv4len { + a := binary.BigEndian.Uint32(ip1) + b := binary.BigEndian.Uint32(ip2) + x := a ^ b + return uint8(bits.LeadingZeros32(x)) + } else if size == net.IPv6len { + a := binary.BigEndian.Uint64(ip1) + b := binary.BigEndian.Uint64(ip2) + x := a ^ b + if x != 0 { + return uint8(bits.LeadingZeros64(x)) + } + a = binary.BigEndian.Uint64(ip1[8:]) + b = binary.BigEndian.Uint64(ip2[8:]) + x = a ^ b + return 64 + uint8(bits.LeadingZeros64(x)) + } else { + panic("Wrong size bit string") + } +} + +func (node *trieEntry) addToPeerEntries() { + node.perPeerElem = node.peer.trieEntries.PushBack(node) +} + +func (node *trieEntry) removeFromPeerEntries() { + if node.perPeerElem != nil { + node.peer.trieEntries.Remove(node.perPeerElem) + node.perPeerElem = nil + } +} + +func (node *trieEntry) choose(ip []byte) byte { + return (ip[node.bitAtByte] >> node.bitAtShift) & 1 +} + +func (node *trieEntry) maskSelf() { + mask := net.CIDRMask(int(node.cidr), len(node.bits)*8) + for i := 0; i < len(mask); i++ { + node.bits[i] &= mask[i] + } +} + +func (node *trieEntry) zeroizePointers() { + // Make the garbage collector's life slightly easier + node.peer = nil + node.child[0] = nil + node.child[1] = nil + node.parent.parentBit = nil +} + +func (node *trieEntry) nodePlacement(ip []byte, cidr uint8) (parent *trieEntry, exact bool) { + for node != nil && node.cidr <= cidr && commonBits(node.bits, ip) >= node.cidr { + parent = node + if parent.cidr == cidr { + exact = true + return + } + bit := node.choose(ip) + node = node.child[bit] + } + return +} + +func (trie parentIndirection) insert(ip []byte, cidr uint8, peer *Peer) { + if *trie.parentBit == nil { + node := &trieEntry{ + peer: peer, + parent: trie, + bits: ip, + cidr: cidr, + bitAtByte: cidr / 8, + bitAtShift: 7 - (cidr % 8), + } + node.maskSelf() + node.addToPeerEntries() + *trie.parentBit = node + return + } + node, exact := (*trie.parentBit).nodePlacement(ip, cidr) + if exact { + node.removeFromPeerEntries() + node.peer = peer + node.addToPeerEntries() + return + } + + newNode := &trieEntry{ + peer: peer, + bits: ip, + cidr: cidr, + bitAtByte: cidr / 8, + bitAtShift: 7 - (cidr % 8), + } + newNode.maskSelf() + newNode.addToPeerEntries() + + var down *trieEntry + if node == nil { + down = *trie.parentBit + } else { + bit := node.choose(ip) + down = node.child[bit] + if down == nil { + newNode.parent = parentIndirection{&node.child[bit], bit} + node.child[bit] = newNode + return + } + } + common := commonBits(down.bits, ip) + if common < cidr { + cidr = common + } + parent := node + + if newNode.cidr == cidr { + bit := newNode.choose(down.bits) + down.parent = parentIndirection{&newNode.child[bit], bit} + newNode.child[bit] = down + if parent == nil { + newNode.parent = trie + *trie.parentBit = newNode + } else { + bit := parent.choose(newNode.bits) + newNode.parent = parentIndirection{&parent.child[bit], bit} + parent.child[bit] = newNode + } + return + } + + node = &trieEntry{ + bits: append([]byte{}, newNode.bits...), + cidr: cidr, + bitAtByte: cidr / 8, + bitAtShift: 7 - (cidr % 8), + } + node.maskSelf() + + bit := node.choose(down.bits) + down.parent = parentIndirection{&node.child[bit], bit} + node.child[bit] = down + bit = node.choose(newNode.bits) + newNode.parent = parentIndirection{&node.child[bit], bit} + node.child[bit] = newNode + if parent == nil { + node.parent = trie + *trie.parentBit = node + } else { + bit := parent.choose(node.bits) + node.parent = parentIndirection{&parent.child[bit], bit} + parent.child[bit] = node + } +} + +func (node *trieEntry) lookup(ip []byte) *Peer { + var found *Peer + size := uint8(len(ip)) + for node != nil && commonBits(node.bits, ip) >= node.cidr { + if node.peer != nil { + found = node.peer + } + if node.bitAtByte == size { + break + } + bit := node.choose(ip) + node = node.child[bit] + } + return found +} + +type AllowedIPs struct { + IPv4 *trieEntry + IPv6 *trieEntry + mutex sync.RWMutex +} + +func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix) bool) { + table.mutex.RLock() + defer table.mutex.RUnlock() + + for elem := peer.trieEntries.Front(); elem != nil; elem = elem.Next() { + node := elem.Value.(*trieEntry) + a, _ := netip.AddrFromSlice(node.bits) + if !cb(netip.PrefixFrom(a, int(node.cidr))) { + return + } + } +} + +func (table *AllowedIPs) RemoveByPeer(peer *Peer) { + table.mutex.Lock() + defer table.mutex.Unlock() + + var next *list.Element + for elem := peer.trieEntries.Front(); elem != nil; elem = next { + next = elem.Next() + node := elem.Value.(*trieEntry) + + node.removeFromPeerEntries() + node.peer = nil + if node.child[0] != nil && node.child[1] != nil { + continue + } + bit := 0 + if node.child[0] == nil { + bit = 1 + } + child := node.child[bit] + if child != nil { + child.parent = node.parent + } + *node.parent.parentBit = child + if node.child[0] != nil || node.child[1] != nil || node.parent.parentBitType > 1 { + node.zeroizePointers() + continue + } + parent := (*trieEntry)(unsafe.Pointer(uintptr(unsafe.Pointer(node.parent.parentBit)) - unsafe.Offsetof(node.child) - unsafe.Sizeof(node.child[0])*uintptr(node.parent.parentBitType))) + if parent.peer != nil { + node.zeroizePointers() + continue + } + child = parent.child[node.parent.parentBitType^1] + if child != nil { + child.parent = parent.parent + } + *parent.parent.parentBit = child + node.zeroizePointers() + parent.zeroizePointers() + } +} + +func (table *AllowedIPs) Insert(prefix netip.Prefix, peer *Peer) { + table.mutex.Lock() + defer table.mutex.Unlock() + + if prefix.Addr().Is6() { + ip := prefix.Addr().As16() + parentIndirection{&table.IPv6, 2}.insert(ip[:], uint8(prefix.Bits()), peer) + } else if prefix.Addr().Is4() { + ip := prefix.Addr().As4() + parentIndirection{&table.IPv4, 2}.insert(ip[:], uint8(prefix.Bits()), peer) + } else { + panic(errors.New("inserting unknown address type")) + } +} + +func (table *AllowedIPs) Lookup(ip []byte) *Peer { + table.mutex.RLock() + defer table.mutex.RUnlock() + switch len(ip) { + case net.IPv6len: + return table.IPv6.lookup(ip) + case net.IPv4len: + return table.IPv4.lookup(ip) + default: + panic(errors.New("looking up unknown address type")) + } +} diff --git a/tempfork/device/allowedips_rand_test.go b/tempfork/device/allowedips_rand_test.go new file mode 100644 index 000000000..07065c30a --- /dev/null +++ b/tempfork/device/allowedips_rand_test.go @@ -0,0 +1,141 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package device + +import ( + "math/rand" + "net" + "net/netip" + "sort" + "testing" +) + +const ( + NumberOfPeers = 100 + NumberOfPeerRemovals = 4 + NumberOfAddresses = 250 + NumberOfTests = 10000 +) + +type SlowNode struct { + peer *Peer + cidr uint8 + bits []byte +} + +type SlowRouter []*SlowNode + +func (r SlowRouter) Len() int { + return len(r) +} + +func (r SlowRouter) Less(i, j int) bool { + return r[i].cidr > r[j].cidr +} + +func (r SlowRouter) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r SlowRouter) Insert(addr []byte, cidr uint8, peer *Peer) SlowRouter { + for _, t := range r { + if t.cidr == cidr && commonBits(t.bits, addr) >= cidr { + t.peer = peer + t.bits = addr + return r + } + } + r = append(r, &SlowNode{ + cidr: cidr, + bits: addr, + peer: peer, + }) + sort.Sort(r) + return r +} + +func (r SlowRouter) Lookup(addr []byte) *Peer { + for _, t := range r { + common := commonBits(t.bits, addr) + if common >= t.cidr { + return t.peer + } + } + return nil +} + +func (r SlowRouter) RemoveByPeer(peer *Peer) SlowRouter { + n := 0 + for _, x := range r { + if x.peer != peer { + r[n] = x + n++ + } + } + return r[:n] +} + +func TestTrieRandom(t *testing.T) { + var slow4, slow6 SlowRouter + var peers []*Peer + var allowedIPs AllowedIPs + + rand.Seed(1) + + for n := 0; n < NumberOfPeers; n++ { + peers = append(peers, &Peer{}) + } + + for n := 0; n < NumberOfAddresses; n++ { + var addr4 [4]byte + rand.Read(addr4[:]) + cidr := uint8(rand.Intn(32) + 1) + index := rand.Intn(NumberOfPeers) + allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom4(addr4), int(cidr)), peers[index]) + slow4 = slow4.Insert(addr4[:], cidr, peers[index]) + + var addr6 [16]byte + rand.Read(addr6[:]) + cidr = uint8(rand.Intn(128) + 1) + index = rand.Intn(NumberOfPeers) + allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(addr6), int(cidr)), peers[index]) + slow6 = slow6.Insert(addr6[:], cidr, peers[index]) + } + + var p int + for p = 0; ; p++ { + for n := 0; n < NumberOfTests; n++ { + var addr4 [4]byte + rand.Read(addr4[:]) + peer1 := slow4.Lookup(addr4[:]) + peer2 := allowedIPs.Lookup(addr4[:]) + if peer1 != peer2 { + t.Errorf("Trie did not match naive implementation, for %v: want %p, got %p", net.IP(addr4[:]), peer1, peer2) + } + + var addr6 [16]byte + rand.Read(addr6[:]) + peer1 = slow6.Lookup(addr6[:]) + peer2 = allowedIPs.Lookup(addr6[:]) + if peer1 != peer2 { + t.Errorf("Trie did not match naive implementation, for %v: want %p, got %p", net.IP(addr6[:]), peer1, peer2) + } + } + if p >= len(peers) || p >= NumberOfPeerRemovals { + break + } + allowedIPs.RemoveByPeer(peers[p]) + slow4 = slow4.RemoveByPeer(peers[p]) + slow6 = slow6.RemoveByPeer(peers[p]) + } + for ; p < len(peers); p++ { + allowedIPs.RemoveByPeer(peers[p]) + } + + if allowedIPs.IPv4 != nil || allowedIPs.IPv6 != nil { + t.Error("Failed to remove all nodes from trie by peer") + } +} diff --git a/tempfork/device/allowedips_test.go b/tempfork/device/allowedips_test.go new file mode 100644 index 000000000..cde068ec3 --- /dev/null +++ b/tempfork/device/allowedips_test.go @@ -0,0 +1,247 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package device + +import ( + "math/rand" + "net" + "net/netip" + "testing" +) + +type testPairCommonBits struct { + s1 []byte + s2 []byte + match uint8 +} + +func TestCommonBits(t *testing.T) { + tests := []testPairCommonBits{ + {s1: []byte{1, 4, 53, 128}, s2: []byte{0, 0, 0, 0}, match: 7}, + {s1: []byte{0, 4, 53, 128}, s2: []byte{0, 0, 0, 0}, match: 13}, + {s1: []byte{0, 4, 53, 253}, s2: []byte{0, 4, 53, 252}, match: 31}, + {s1: []byte{192, 168, 1, 1}, s2: []byte{192, 169, 1, 1}, match: 15}, + {s1: []byte{65, 168, 1, 1}, s2: []byte{192, 169, 1, 1}, match: 0}, + } + + for _, p := range tests { + v := commonBits(p.s1, p.s2) + if v != p.match { + t.Error( + "For slice", p.s1, p.s2, + "expected match", p.match, + ",but got", v, + ) + } + } +} + +func benchmarkTrie(peerNumber, addressNumber, addressLength int, b *testing.B) { + var trie *trieEntry + var peers []*Peer + root := parentIndirection{&trie, 2} + + rand.Seed(1) + + const AddressLength = 4 + + for n := 0; n < peerNumber; n++ { + peers = append(peers, &Peer{}) + } + + for n := 0; n < addressNumber; n++ { + var addr [AddressLength]byte + rand.Read(addr[:]) + cidr := uint8(rand.Uint32() % (AddressLength * 8)) + index := rand.Int() % peerNumber + root.insert(addr[:], cidr, peers[index]) + } + + for n := 0; n < b.N; n++ { + var addr [AddressLength]byte + rand.Read(addr[:]) + trie.lookup(addr[:]) + } +} + +func BenchmarkTrieIPv4Peers100Addresses1000(b *testing.B) { + benchmarkTrie(100, 1000, net.IPv4len, b) +} + +func BenchmarkTrieIPv4Peers10Addresses10(b *testing.B) { + benchmarkTrie(10, 10, net.IPv4len, b) +} + +func BenchmarkTrieIPv6Peers100Addresses1000(b *testing.B) { + benchmarkTrie(100, 1000, net.IPv6len, b) +} + +func BenchmarkTrieIPv6Peers10Addresses10(b *testing.B) { + benchmarkTrie(10, 10, net.IPv6len, b) +} + +/* Test ported from kernel implementation: + * selftest/allowedips.h + */ +func TestTrieIPv4(t *testing.T) { + a := &Peer{} + b := &Peer{} + c := &Peer{} + d := &Peer{} + e := &Peer{} + g := &Peer{} + h := &Peer{} + + var allowedIPs AllowedIPs + + insert := func(peer *Peer, a, b, c, d byte, cidr uint8) { + allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom4([4]byte{a, b, c, d}), int(cidr)), peer) + } + + assertEQ := func(peer *Peer, a, b, c, d byte) { + p := allowedIPs.Lookup([]byte{a, b, c, d}) + if p != peer { + t.Error("Assert EQ failed") + } + } + + assertNEQ := func(peer *Peer, a, b, c, d byte) { + p := allowedIPs.Lookup([]byte{a, b, c, d}) + if p == peer { + t.Error("Assert NEQ failed") + } + } + + insert(a, 192, 168, 4, 0, 24) + insert(b, 192, 168, 4, 4, 32) + insert(c, 192, 168, 0, 0, 16) + insert(d, 192, 95, 5, 64, 27) + insert(c, 192, 95, 5, 65, 27) + insert(e, 0, 0, 0, 0, 0) + insert(g, 64, 15, 112, 0, 20) + insert(h, 64, 15, 123, 211, 25) + insert(a, 10, 0, 0, 0, 25) + insert(b, 10, 0, 0, 128, 25) + insert(a, 10, 1, 0, 0, 30) + insert(b, 10, 1, 0, 4, 30) + insert(c, 10, 1, 0, 8, 29) + insert(d, 10, 1, 0, 16, 29) + + assertEQ(a, 192, 168, 4, 20) + assertEQ(a, 192, 168, 4, 0) + assertEQ(b, 192, 168, 4, 4) + assertEQ(c, 192, 168, 200, 182) + assertEQ(c, 192, 95, 5, 68) + assertEQ(e, 192, 95, 5, 96) + assertEQ(g, 64, 15, 116, 26) + assertEQ(g, 64, 15, 127, 3) + + insert(a, 1, 0, 0, 0, 32) + insert(a, 64, 0, 0, 0, 32) + insert(a, 128, 0, 0, 0, 32) + insert(a, 192, 0, 0, 0, 32) + insert(a, 255, 0, 0, 0, 32) + + assertEQ(a, 1, 0, 0, 0) + assertEQ(a, 64, 0, 0, 0) + assertEQ(a, 128, 0, 0, 0) + assertEQ(a, 192, 0, 0, 0) + assertEQ(a, 255, 0, 0, 0) + + allowedIPs.RemoveByPeer(a) + + assertNEQ(a, 1, 0, 0, 0) + assertNEQ(a, 64, 0, 0, 0) + assertNEQ(a, 128, 0, 0, 0) + assertNEQ(a, 192, 0, 0, 0) + assertNEQ(a, 255, 0, 0, 0) + + allowedIPs.RemoveByPeer(a) + allowedIPs.RemoveByPeer(b) + allowedIPs.RemoveByPeer(c) + allowedIPs.RemoveByPeer(d) + allowedIPs.RemoveByPeer(e) + allowedIPs.RemoveByPeer(g) + allowedIPs.RemoveByPeer(h) + if allowedIPs.IPv4 != nil || allowedIPs.IPv6 != nil { + t.Error("Expected removing all the peers to empty trie, but it did not") + } + + insert(a, 192, 168, 0, 0, 16) + insert(a, 192, 168, 0, 0, 24) + + allowedIPs.RemoveByPeer(a) + + assertNEQ(a, 192, 168, 0, 1) +} + +/* Test ported from kernel implementation: + * selftest/allowedips.h + */ +func TestTrieIPv6(t *testing.T) { + a := &Peer{} + b := &Peer{} + c := &Peer{} + d := &Peer{} + e := &Peer{} + f := &Peer{} + g := &Peer{} + h := &Peer{} + + var allowedIPs AllowedIPs + + expand := func(a uint32) []byte { + var out [4]byte + out[0] = byte(a >> 24 & 0xff) + out[1] = byte(a >> 16 & 0xff) + out[2] = byte(a >> 8 & 0xff) + out[3] = byte(a & 0xff) + return out[:] + } + + insert := func(peer *Peer, a, b, c, d uint32, cidr uint8) { + var addr []byte + addr = append(addr, expand(a)...) + addr = append(addr, expand(b)...) + addr = append(addr, expand(c)...) + addr = append(addr, expand(d)...) + allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(*(*[16]byte)(addr)), int(cidr)), peer) + } + + assertEQ := func(peer *Peer, a, b, c, d uint32) { + var addr []byte + addr = append(addr, expand(a)...) + addr = append(addr, expand(b)...) + addr = append(addr, expand(c)...) + addr = append(addr, expand(d)...) + p := allowedIPs.Lookup(addr) + if p != peer { + t.Error("Assert EQ failed") + } + } + + insert(d, 0x26075300, 0x60006b00, 0, 0xc05f0543, 128) + insert(c, 0x26075300, 0x60006b00, 0, 0, 64) + insert(e, 0, 0, 0, 0, 0) + insert(f, 0, 0, 0, 0, 0) + insert(g, 0x24046800, 0, 0, 0, 32) + insert(h, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef, 64) + insert(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef, 128) + insert(c, 0x24446800, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + insert(b, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) + + assertEQ(d, 0x26075300, 0x60006b00, 0, 0xc05f0543) + assertEQ(c, 0x26075300, 0x60006b00, 0, 0xc02e01ee) + assertEQ(f, 0x26075300, 0x60006b01, 0, 0) + assertEQ(g, 0x24046800, 0x40040806, 0, 0x1006) + assertEQ(g, 0x24046800, 0x40040806, 0x1234, 0x5678) + assertEQ(f, 0x240467ff, 0x40040806, 0x1234, 0x5678) + assertEQ(f, 0x24046801, 0x40040806, 0x1234, 0x5678) + assertEQ(h, 0x24046800, 0x40040800, 0x1234, 0x5678) + assertEQ(h, 0x24046800, 0x40040800, 0, 0) + assertEQ(h, 0x24046800, 0x40040800, 0x10101010, 0x10101010) + assertEQ(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef) +} diff --git a/tempfork/device/peer.go b/tempfork/device/peer.go new file mode 100644 index 000000000..371e90ac7 --- /dev/null +++ b/tempfork/device/peer.go @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package device + +import ( + "container/list" +) + +type Peer struct { + trieEntries list.List +}