├── messages.go
├── systemd
└── meshboi_rollodex.service
├── go.mod
├── rolodex_test.go
├── dtls_config_generator.go
├── .github
└── workflows
│ └── go.yml
├── tun_router_test.go
├── LICENSE.md
├── peer_conn_test.go
├── peer_connector_test.go
├── rolodex_client_test.go
├── peer_conn_store.go
├── tun_router.go
├── scripts
└── smoketest.sh
├── peer_conn.go
├── meshboi_client.go
├── README.md
├── rolodex_client.go
├── tun.go
├── meshboi_client_test.go
├── peer_conn_store_test.go
├── multiplexed_dtls_conn.go
├── cmd
└── meshboi
│ └── main.go
├── rolodex.go
├── peer_connector.go
└── go.sum
/messages.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import "inet.af/netaddr"
4 |
5 | type HeartbeatMessage struct {
6 | NetworkName string
7 | }
8 |
9 | type NetworkMap struct {
10 | Addresses []netaddr.IPPort
11 | YourIndex int
12 | }
13 |
--------------------------------------------------------------------------------
/systemd/meshboi_rollodex.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=meshboi rolodex
3 | Wants=basic.target
4 | After=basic.target network.target
5 | Before=sshd.service
6 |
7 | [Service]
8 | Type=simple
9 | ExecStart=/usr/local/bin/meshboi rolodex
10 | Restart=always
11 |
12 | [Install]
13 | WantedBy=multi-user.target
14 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/samvrlewis/meshboi
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/kr/pretty v0.1.0 // indirect
7 | github.com/pion/dtls/v2 v2.0.8
8 | github.com/samvrlewis/udp v0.1.1-0.20210505081938-3a6139185318
9 | github.com/sirupsen/logrus v1.8.1
10 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777
11 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
12 | inet.af/netaddr v0.0.0-20210313195008-843b4240e319
13 | )
14 |
--------------------------------------------------------------------------------
/rolodex_test.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "bytes"
5 | "net"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestRolodex(t *testing.T) {
11 | conn, _ := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 33333})
12 | rollo, err := NewRolodex(conn, 1*time.Second, 5*time.Second)
13 |
14 | if err != nil {
15 | t.FailNow()
16 | }
17 |
18 | go rollo.Run()
19 |
20 | client, err := net.Dial("udp", "127.0.0.1:33333")
21 |
22 | client.Write([]byte(`{"networkName": "test"}`))
23 |
24 | time.Sleep(2 * time.Second)
25 |
26 | buf := make([]byte, 1000)
27 |
28 | client.Read(buf)
29 |
30 | if !bytes.Contains(buf, []byte("127.0.0.1")) {
31 | t.Fatalf("Didn't get back expected IP")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/dtls_config_generator.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "github.com/pion/dtls/v2"
5 | "inet.af/netaddr"
6 | )
7 |
8 | func getDtlsConfig(vpnIp netaddr.IP, psk []byte) *dtls.Config {
9 | return &dtls.Config{
10 | PSK: func(hint []byte) ([]byte, error) {
11 | return psk, nil
12 | },
13 | // We set the PSK identity hint as the IP address of this member in the
14 | // VPN as an quick and hacky way of signalling (out of band) who this
15 | // member is to other members we connect to. A more robust way of
16 | // achieving this would be to define an OOB messaging scheme to do this
17 | // with instead.
18 | PSKIdentityHint: []byte(vpnIp.String()),
19 | CipherSuites: []dtls.CipherSuiteID{dtls.TLS_PSK_WITH_AES_128_CCM_8},
20 | ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 | on:
3 | push:
4 | branches: [ main ]
5 | pull_request:
6 | branches: [ main ]
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Set up Go
15 | uses: actions/setup-go@v2
16 | with:
17 | go-version: 1.15
18 |
19 | - name: Build
20 | run: |
21 | mkdir artifacts
22 | go build -o artifacts -v ./...
23 |
24 | - name: Test
25 | run: go test -v ./...
26 |
27 | - name: Upload artifacts
28 | uses: actions/upload-artifact@v2
29 | with:
30 | name: meshboi
31 | path: artifacts/meshboi
32 |
33 | integration-test:
34 | container:
35 | image: golang:1.16.3-buster
36 | options: --privileged
37 | runs-on: ubuntu-latest
38 |
39 | steps:
40 | - uses: actions/checkout@v2
41 |
42 | - name: Integration test
43 | run: ./scripts/smoketest.sh
44 |
--------------------------------------------------------------------------------
/tun_router_test.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 | "reflect"
6 | "testing"
7 |
8 | "golang.org/x/net/ipv4"
9 | "inet.af/netaddr"
10 | )
11 |
12 | func TestRouter(t *testing.T) {
13 | store := NewPeerConnStore()
14 | tunClient, tunServer := net.Pipe()
15 | tr := NewTunRouter(tunClient, store)
16 | go tr.Run()
17 | defer tr.Stop()
18 |
19 | peerClient, peerServer := net.Pipe()
20 |
21 | peer := NewPeerConn(netaddr.MustParseIP("192.168.4.3"), netaddr.MustParseIPPort("192.152.12.2:2222"), peerClient, tunClient)
22 | go peer.readLoop()
23 | go peer.sendLoop()
24 | store.Add(&peer)
25 |
26 | hdr := ipv4.Header{
27 | Src: net.ParseIP("192.168.4.2"),
28 | Dst: net.ParseIP("192.168.4.3"),
29 | Len: 20,
30 | Version: 4,
31 | }
32 |
33 | hdrBytes, _ := hdr.Marshal()
34 |
35 | msg := append(hdrBytes[:], []byte("hello")...)
36 |
37 | tunServer.Write(msg)
38 |
39 | readBytes := make([]byte, 1000)
40 |
41 | n, _ := peerServer.Read(readBytes)
42 |
43 | if !reflect.DeepEqual(readBytes[:n], msg) {
44 | t.Errorf("Messages not equal")
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Sam Lewis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/peer_conn_test.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 | "reflect"
6 | "testing"
7 |
8 | "inet.af/netaddr"
9 | )
10 |
11 | // Tests that queued data goes to the peer
12 | func TestSendData(t *testing.T) {
13 | client, server := net.Pipe()
14 | tun := FakeTun{}
15 | conn := NewPeerConn(netaddr.MustParseIP("192.168.5.1"), netaddr.MustParseIPPort("192.168.33.1:5000"), client, &tun)
16 | msg := []byte("hello this is some data")
17 |
18 | go conn.sendLoop()
19 |
20 | conn.QueueData(msg)
21 |
22 | b := make([]byte, 1000)
23 | n, _ := server.Read(b)
24 |
25 | if !reflect.DeepEqual(b[:n], msg) {
26 | t.Fatalf("Didn't read expected data")
27 | }
28 | }
29 |
30 | // Tests that data received externally from the peer goes to the tun
31 | func TestReceiveData(t *testing.T) {
32 | client, server := net.Pipe()
33 | tunClient, tunServer := net.Pipe()
34 | conn := NewPeerConn(netaddr.MustParseIP("192.168.5.1"), netaddr.MustParseIPPort("192.168.33.1:5000"), client, tunClient)
35 | go conn.readLoop()
36 |
37 | msg := []byte("hello this is some data")
38 | server.Write(msg)
39 | b := make([]byte, 1000)
40 | n, _ := tunServer.Read(b)
41 |
42 | if !reflect.DeepEqual(b[:n], msg) {
43 | t.Fatalf("Didn't read expected data %v %v", b[:n], msg)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/peer_connector_test.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 | "testing"
6 |
7 | "inet.af/netaddr"
8 | )
9 |
10 | type testListenerDialer struct {
11 | dialer net.Dialer
12 | dialed chan (net.Addr)
13 | }
14 |
15 | func (t testListenerDialer) DialMesh(raddr net.Addr) (MeshConn, error) {
16 | t.dialed <- raddr
17 | c, _ := net.Pipe()
18 | ip := netaddr.MustParseIP("192.168.1.1")
19 | return &meshConn{
20 | Conn: c,
21 | remoteMeshAddr: ip,
22 | }, nil
23 | }
24 |
25 | func (t testListenerDialer) AcceptMesh() (MeshConn, error) {
26 | return nil, nil
27 | }
28 |
29 | func (t testListenerDialer) Dial(raddr net.Addr) (net.Conn, error) {
30 | fakeConn, _ := net.Pipe()
31 | return fakeConn, nil
32 | }
33 |
34 | func TestPeerConnector(t *testing.T) {
35 | td := testListenerDialer{dialed: make(chan net.Addr)}
36 | store := NewPeerConnStore()
37 | client, _ := net.Pipe()
38 |
39 | pc := NewPeerConnector(td, store, client)
40 |
41 | nm := NetworkMap{
42 | Addresses: []netaddr.IPPort{netaddr.MustParseIPPort("192.168.33.1:3000"),
43 | netaddr.MustParseIPPort("192.168.33.2:4000")},
44 | YourIndex: 0,
45 | }
46 |
47 | go pc.OnNetworkMapUpdate(nm)
48 |
49 | dialed := <-td.dialed
50 |
51 | if dialed.String() != "192.168.33.2:4000" {
52 | t.Fatalf("Dialed wrong address")
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/rolodex_client_test.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "bytes"
5 | "net"
6 | "testing"
7 | "time"
8 |
9 | "inet.af/netaddr"
10 | )
11 |
12 | func TestNetworkCallback(t *testing.T) {
13 | var nmap NetworkMap
14 |
15 | called := 0
16 | callback := func(member NetworkMap) {
17 | called += 1
18 | nmap = member
19 | }
20 | client, server := net.Pipe()
21 | rolloClient := NewRolodexClient("testNet", client, time.Second, callback)
22 |
23 | go rolloClient.Run()
24 | defer rolloClient.Stop()
25 |
26 | server.Write([]byte(`{ "addresses": ["192.168.4.1:2000"], "your_index": 0 }`))
27 |
28 | time.Sleep(time.Millisecond)
29 | if len(nmap.Addresses) != 1 {
30 | t.Fatalf("expected 1 address but got %v", len(nmap.Addresses))
31 | }
32 |
33 | if called != 1 {
34 | t.Fatalf("Called more than once")
35 | }
36 |
37 | if nmap.Addresses[0] != netaddr.MustParseIPPort("192.168.4.1:2000") {
38 | t.Fatalf("Wrong ip address back")
39 | }
40 |
41 | if nmap.YourIndex != 0 {
42 | t.Fatalf("Wrong index back")
43 | }
44 | }
45 |
46 | func TestClientSendsHeartBeat(t *testing.T) {
47 | callback := func(member NetworkMap) {
48 | }
49 | client, server := net.Pipe()
50 | rolloClient := NewRolodexClient("testNet", client, time.Millisecond, callback)
51 | go rolloClient.Run()
52 | defer rolloClient.Stop()
53 |
54 | time.Sleep(time.Millisecond)
55 |
56 | b := make([]byte, 1000)
57 | n, _ := server.Read(b)
58 |
59 | if !bytes.Contains(b[:n], []byte("testNet")) {
60 | t.Fatalf("Didn't contain the network name %v", string(b[:n]))
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/peer_conn_store.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "sync"
5 |
6 | "inet.af/netaddr"
7 | )
8 |
9 | type PeerConnStore struct {
10 | peersByOutsideIPPort map[netaddr.IPPort]*PeerConn
11 | peersByInsideIP map[netaddr.IP]*PeerConn
12 | lock sync.RWMutex
13 | }
14 |
15 | func NewPeerConnStore() *PeerConnStore {
16 | s := &PeerConnStore{}
17 | s.peersByInsideIP = make(map[netaddr.IP]*PeerConn)
18 | s.peersByOutsideIPPort = make(map[netaddr.IPPort]*PeerConn)
19 |
20 | return s
21 | }
22 |
23 | func (p *PeerConnStore) Add(peer *PeerConn) {
24 | p.lock.Lock()
25 | defer p.lock.Unlock()
26 |
27 | p.peersByInsideIP[peer.insideIP] = peer
28 | p.peersByOutsideIPPort[peer.outsideAddr] = peer
29 | }
30 |
31 | func (p *PeerConnStore) GetByInsideIp(insideIP netaddr.IP) (*PeerConn, bool) {
32 | p.lock.Lock()
33 | defer p.lock.Unlock()
34 |
35 | peer, ok := p.peersByInsideIP[insideIP]
36 |
37 | return peer, ok
38 | }
39 |
40 | func (p *PeerConnStore) GetByOutsideIpPort(outsideIPPort netaddr.IPPort) (*PeerConn, bool) {
41 | p.lock.Lock()
42 | defer p.lock.Unlock()
43 |
44 | peer, ok := p.peersByOutsideIPPort[outsideIPPort]
45 |
46 | return peer, ok
47 | }
48 |
49 | func (p *PeerConnStore) RemoveByOutsideIPPort(outsideIPPort netaddr.IPPort) bool {
50 | peer, ok := p.GetByOutsideIpPort(outsideIPPort)
51 |
52 | if !ok {
53 | return false
54 | }
55 |
56 | p.lock.Lock()
57 | defer p.lock.Unlock()
58 |
59 | insideIp := peer.insideIP
60 |
61 | delete(p.peersByInsideIP, insideIp)
62 | delete(p.peersByOutsideIPPort, outsideIPPort)
63 |
64 | return true
65 | }
66 |
--------------------------------------------------------------------------------
/tun_router.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 |
6 | "golang.org/x/net/ipv4"
7 | "inet.af/netaddr"
8 |
9 | log "github.com/sirupsen/logrus"
10 | )
11 |
12 | type TunRouter struct {
13 | tun TunConn
14 | store *PeerConnStore
15 | stopped bool
16 | }
17 |
18 | func NewTunRouter(tun TunConn, store *PeerConnStore) TunRouter {
19 | return TunRouter{
20 | tun: tun,
21 | store: store,
22 | stopped: false,
23 | }
24 | }
25 |
26 | func (tr *TunRouter) Run() {
27 | packet := make([]byte, bufSize)
28 |
29 | for {
30 | n, err := tr.tun.Read(packet)
31 | if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
32 | log.Warn("Temporary error reading from tun device, continuing: ", nerr)
33 | continue
34 | }
35 |
36 | if err != nil {
37 | if !tr.stopped {
38 | log.Fatalln("Serious error reading from tun device: ", err)
39 | }
40 | break
41 | }
42 |
43 | header, err := ipv4.ParseHeader(packet[:n])
44 |
45 | if err != nil {
46 | log.Error("Error parsing ipv4 header of tun packet: ", err)
47 | continue
48 | }
49 |
50 | vpnIP, ok := netaddr.FromStdIP(header.Dst)
51 |
52 | if !ok {
53 | log.Error("Error converting to netaddr IP")
54 | continue
55 | }
56 |
57 | peer, ok := tr.store.GetByInsideIp(vpnIP)
58 |
59 | if !ok {
60 | log.Warn("Dropping data destined for ", vpnIP)
61 | continue
62 | }
63 |
64 | msg := make([]byte, n)
65 | copy(msg, packet[:n])
66 |
67 | peer.QueueData(msg)
68 | }
69 | }
70 |
71 | func (tr *TunRouter) Stop() error {
72 | tr.stopped = true
73 | return tr.tun.Close()
74 | }
75 |
--------------------------------------------------------------------------------
/scripts/smoketest.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Sets up a blue NS and a green NS with a veth pair to allow direct
4 | # communications. The idea is that this allows for testing of tipod, with the
5 | # green running in the green namespace and the blue running in the blue
6 | # namespace. The green and blue can communicate directly through the veth
7 | # link.
8 | set -x
9 |
10 | trap "exit" INT TERM
11 | trap "pkill -P $$; exit 0" EXIT
12 |
13 | go build -o meshboi cmd/meshboi/main.go
14 |
15 | ip netns add blue
16 | ip netns add green
17 | ip link add veth0 type veth peer name veth1
18 | ip link set veth0 netns blue
19 | ip link set veth1 netns green
20 |
21 | ip netns exec blue ip addr add 10.1.1.1/24 dev veth0
22 | ip netns exec blue ip link set dev veth0 up
23 |
24 | ip netns exec green ip addr add 10.1.1.2/24 dev veth1
25 | ip netns exec green ip link set dev veth1 up
26 |
27 | ip netns exec blue ip link set lo up
28 | ip netns exec green ip link set lo up
29 |
30 | ip netns exec blue ./meshboi rolodex -listen-address 10.1.1.1 &
31 | ip netns exec blue ./meshboi client -rolodex-address 10.1.1.1 -vpn-ip 192.168.50.1/24 -psk testpassword -network testnetwork &
32 | ip netns exec green ./meshboi client -rolodex-address 10.1.1.1 -vpn-ip 192.168.50.2/24 -psk testpassword -network testnetwork &
33 |
34 | sleep 5
35 |
36 | ip netns exec blue ping -c 1 192.168.50.2
37 |
38 | if [[ $? -ne 0 ]] ; then
39 | echo "Not successful blue to green"
40 | exit 1
41 | fi
42 |
43 | ip netns exec green ping -c 1 192.168.50.1
44 |
45 | if [[ $? -ne 0 ]] ; then
46 | echo "Not successful green to blue"
47 | exit 1
48 | fi
49 |
50 | echo "Success!"
51 |
52 | exit 0
--------------------------------------------------------------------------------
/peer_conn.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 | "time"
6 |
7 | log "github.com/sirupsen/logrus"
8 | "inet.af/netaddr"
9 | )
10 |
11 | const bufSize = 65535
12 |
13 | // Represents a connection to a peer
14 | type PeerConn struct {
15 | // The IP address within the VPN
16 | insideIP netaddr.IP
17 |
18 | // The IP address over the internet
19 | outsideAddr netaddr.IPPort
20 |
21 | // Time of last contact
22 | lastContacted time.Time
23 |
24 | // the connection to the peer
25 | conn net.Conn
26 | outgoing chan []byte
27 | tun TunConn
28 | }
29 |
30 | func NewPeerConn(insideIP netaddr.IP, outsideAddr netaddr.IPPort, conn net.Conn, tun TunConn) PeerConn {
31 | return PeerConn{
32 | insideIP: insideIP, // maybe these dont need to be inside the peer. could just be in the peer store
33 | outsideAddr: outsideAddr,
34 | conn: conn,
35 | tun: tun,
36 | lastContacted: time.Now(),
37 | outgoing: make(chan []byte),
38 | }
39 | }
40 |
41 | func (p *PeerConn) QueueData(data []byte) {
42 | p.outgoing <- data
43 | }
44 |
45 | func (p *PeerConn) readLoop() {
46 | b := make([]byte, bufSize)
47 | for {
48 | n, err := p.conn.Read(b)
49 | if err != nil {
50 | panic(err)
51 | }
52 |
53 | p.lastContacted = time.Now()
54 | written, err := p.tun.Write(b[:n])
55 |
56 | if err != nil {
57 | panic(err)
58 | }
59 |
60 | if written != n {
61 | log.Warn("Not all data written to tun")
62 | }
63 | }
64 | }
65 |
66 | // Chat starts the stdin readloop to dispatch messages to the hub
67 | func (p *PeerConn) sendLoop() {
68 | for {
69 | data := <-p.outgoing
70 | n, err := p.conn.Write(data)
71 |
72 | if err != nil {
73 | log.Error("Error sending over UDP conn: ", err)
74 | continue
75 | }
76 |
77 | if n != len(data) {
78 | log.Warn("Not all data written to peer")
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/meshboi_client.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 | "sync"
6 | "time"
7 |
8 | log "github.com/sirupsen/logrus"
9 | "inet.af/netaddr"
10 | )
11 |
12 | type MeshboiClient struct {
13 | peerStore *PeerConnStore
14 | rolloClient RolodexClient
15 | tunRouter TunRouter
16 | peerConnector PeerConnector
17 | }
18 |
19 | func NewMeshBoiClient(tun TunConn, vpnIpPrefix netaddr.IPPrefix, rolodexIP netaddr.IP, rolodexPort int, networkName string, meshPSK []byte) (*MeshboiClient, error) {
20 | listenAddr := &net.UDPAddr{IP: net.ParseIP("0.0.0.0")}
21 | dtlsConfig := getDtlsConfig(vpnIpPrefix.IP, meshPSK)
22 |
23 | multiplexConn, err := NewMultiplexedDTLSConn(listenAddr, dtlsConfig)
24 |
25 | if err != nil {
26 | log.Error("Error creating multiplexed conn ", err)
27 | return nil, err
28 | }
29 |
30 | rolodexAddr := &net.UDPAddr{IP: rolodexIP.IPAddr().IP, Port: rolodexPort}
31 | rolodexConn, err := multiplexConn.Dial(rolodexAddr)
32 |
33 | if err != nil {
34 | log.Error("Error connecting to rolodex server")
35 | return nil, err
36 | }
37 |
38 | mc := MeshboiClient{}
39 |
40 | mc.peerStore = NewPeerConnStore()
41 | mc.peerConnector = NewPeerConnector(multiplexConn, mc.peerStore, tun)
42 | mc.rolloClient = NewRolodexClient(networkName, rolodexConn, time.Duration(5*time.Second), mc.peerConnector.OnNetworkMapUpdate)
43 | mc.tunRouter = NewTunRouter(tun, mc.peerStore)
44 |
45 | return &mc, nil
46 | }
47 |
48 | func (mc *MeshboiClient) Run() {
49 | var wg sync.WaitGroup
50 | wg.Add(3)
51 |
52 | go func() {
53 | mc.tunRouter.Run()
54 | wg.Done()
55 | }()
56 |
57 | go func() {
58 | mc.peerConnector.ListenForPeers()
59 | wg.Done()
60 | }()
61 |
62 | go func() {
63 | mc.rolloClient.Run()
64 | wg.Done()
65 | }()
66 |
67 | wg.Wait()
68 | }
69 |
70 | func (mc *MeshboiClient) Stop() {
71 | mc.rolloClient.Stop()
72 | mc.peerConnector.Stop()
73 | mc.tunRouter.Stop()
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  meshboi
2 |
3 | meshboi is a toy mesh VPN implementation, created for fun and learning purposes. It allows the creation of peer to peer networks over the internet in a similar fashion to tools such as [Nebula](https://github.com/slackhq/nebula) and [Tailscale](https://tailscale.com/).
4 |
5 | More information about how meshboi works is available on my blog post [Creating a mesh VPN tool for fun and learning](https://www.samlewis.me/2021/07/creating-mesh-vpn-tool-for-fun/).
6 |
7 | ## Quick Start
8 |
9 | 1. Download the most recent [release](https://github.com/samvrlewis/meshboi/releases).
10 | 2. Start meshboi on one host:
11 |
12 | ```
13 | ./meshboi client -rolodex-address rolodex.samlewis.me -vpn-ip 192.168.50.1/24 -psk -network
14 | ```
15 |
16 | 3. Start meshboi on another host:
17 |
18 | ```
19 | ./meshboi client -rolodex-address rolodex.samlewis.me -vpn-ip 192.168.50.2/24 -psk -network
20 | ```
21 |
22 | 4. The hosts should now be able to communicate as though they were on the same LAN!
23 |
24 | Note that this will use the publicly accessible rolodex server that I host. No user data flows through this server other than metadata that contains the internet IP and ports of your instances (though this has not been properly audited, so please use at your own risk!). You are also free to host your own Rolodex server on an an internet accessible server (a cheap EC2 instance or equivalent will work fine). You can do so with:
25 |
26 | ```
27 | ./meshboi rolodex
28 | ```
29 |
30 | And then use the IP address or hostname of this server when starting meshboi in client mode (with the `-rolodex-address` option).
31 |
32 | ## Demo
33 |
34 | An asciinema recording of meshboi in action:
35 |
36 | [](https://asciinema.org/a/Cux2gxc8VusS0QbL3tkmWLFb4)
--------------------------------------------------------------------------------
/rolodex_client.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "encoding/json"
5 | "net"
6 | "sync"
7 | "time"
8 |
9 | log "github.com/sirupsen/logrus"
10 | )
11 |
12 | type RolodexCallback func(member NetworkMap)
13 |
14 | type RolodexClient struct {
15 | networkName string
16 | conn net.Conn
17 | sendRate time.Duration
18 | callback RolodexCallback
19 | quit chan bool
20 | wg *sync.WaitGroup
21 | }
22 |
23 | func NewRolodexClient(networkName string, conn net.Conn, sendRate time.Duration, callback RolodexCallback) RolodexClient {
24 | client := RolodexClient{
25 | networkName: networkName,
26 | conn: conn,
27 | sendRate: sendRate,
28 | callback: callback,
29 | quit: make(chan bool),
30 | wg: &sync.WaitGroup{},
31 | }
32 |
33 | return client
34 | }
35 |
36 | func (c *RolodexClient) Run() {
37 | go c.readLoop()
38 | go c.sendLoop()
39 | c.wg.Add(2)
40 | c.wg.Wait()
41 | }
42 |
43 | func (c *RolodexClient) readLoop() {
44 | defer c.wg.Done()
45 |
46 | buf := make([]byte, 65535)
47 | for {
48 | n, err := c.conn.Read(buf)
49 |
50 | if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
51 | log.Warn("Temporary error reading from rolloConn: ", nerr)
52 | continue
53 | }
54 |
55 | if err != nil {
56 | log.Error("Unrecoverable error: ", err)
57 | break
58 | }
59 |
60 | var members NetworkMap
61 |
62 | if err := json.Unmarshal(buf[:n], &members); err != nil {
63 | log.Error("Error unmarshalling incoming message: ", err.Error())
64 | continue
65 | }
66 |
67 | c.callback(members)
68 | }
69 | }
70 |
71 | func (c *RolodexClient) sendLoop() {
72 | defer c.wg.Done()
73 |
74 | ticker := time.NewTicker(c.sendRate)
75 | for {
76 | heartbeat := HeartbeatMessage{NetworkName: c.networkName}
77 | b, err := json.Marshal(heartbeat)
78 | if err != nil {
79 | log.Fatalln("Error marshalling JSON heartbeat message: ", err)
80 | }
81 |
82 | _, err = c.conn.Write(b)
83 |
84 | if err != nil {
85 | log.Error("Error sending heartbeat over the rollo conn: ", err)
86 | }
87 |
88 | select {
89 | case <-c.quit:
90 | return
91 | case <-ticker.C:
92 | break
93 | }
94 | }
95 | }
96 |
97 | func (c *RolodexClient) Stop() {
98 | c.conn.Close()
99 | c.quit <- true
100 | }
101 |
--------------------------------------------------------------------------------
/tun.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "io"
5 | "os"
6 | "os/exec"
7 | "strconv"
8 | "syscall"
9 | "unsafe"
10 | )
11 |
12 | const (
13 | IFF_TUN = 0x1 /* Flag to open a TUN device (rather than TAP) */
14 | IFF_NO_PI = 0x1000 /* Do not provide packet information */
15 | )
16 |
17 | type ifReq struct {
18 | Name [16]byte
19 | Flags uint16
20 | }
21 |
22 | type Tun struct {
23 | io.ReadWriteCloser
24 | Name string
25 | }
26 |
27 | type TunConn interface {
28 | io.ReadWriteCloser
29 | }
30 |
31 | //https://www.kernel.org/doc/Documentation/networking/tuntap.txt
32 | func NewTun(name string) (*Tun, error) {
33 | tunFile, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
34 |
35 | if err != nil {
36 | return nil, err
37 | }
38 | req := ifReq{}
39 | req.Flags = IFF_TUN | IFF_NO_PI
40 | copy(req.Name[:], name)
41 |
42 | _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, tunFile.Fd(), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&req)))
43 |
44 | if errno != 0 {
45 | return nil, os.NewSyscallError("ioctl", errno)
46 | }
47 |
48 | tun := Tun{
49 | Name: name,
50 | ReadWriteCloser: tunFile,
51 | }
52 |
53 | return &tun, nil
54 | }
55 |
56 | // Makes a Tun with the desired config and immediately sets it up
57 | func NewTunWithConfig(name string, ip string, mtu int) (*Tun, error) {
58 | tun, err := NewTun(name)
59 |
60 | if err != nil {
61 | return nil, err
62 | }
63 |
64 | if err := tun.SetNetwork(ip); err != nil {
65 | return nil, err
66 | }
67 |
68 | if err := tun.SetMtu(mtu); err != nil {
69 | return nil, err
70 | }
71 |
72 | if err := tun.SetLinkUp(); err != nil {
73 | return nil, err
74 | }
75 |
76 | return tun, nil
77 | }
78 |
79 | func (t Tun) SetLinkUp() error {
80 | cmd := exec.Command("/sbin/ip", "link", "set", t.Name, "up")
81 |
82 | if err := cmd.Run(); err != nil {
83 | return err
84 | }
85 |
86 | return nil
87 | }
88 |
89 | func (t Tun) SetNetwork(ip string) error {
90 | cmd := exec.Command("/sbin/ip", "addr", "add", ip, "dev", t.Name)
91 |
92 | if err := cmd.Run(); err != nil {
93 | return err
94 | }
95 |
96 | return nil
97 | }
98 |
99 | func (t Tun) SetMtu(mtu int) error {
100 | cmd := exec.Command("/sbin/ip", "link", "set", "dev", t.Name, "mtu", strconv.Itoa(mtu))
101 |
102 | if err := cmd.Run(); err != nil {
103 | return err
104 | }
105 |
106 | return nil
107 | }
108 |
--------------------------------------------------------------------------------
/meshboi_client_test.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 | "reflect"
6 | "testing"
7 | "time"
8 |
9 | "golang.org/x/net/ipv4"
10 | "inet.af/netaddr"
11 | )
12 |
13 | // Simple test to test data flows from one client to another
14 | func TestTwoClients(t *testing.T) {
15 |
16 | conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345})
17 |
18 | if err != nil {
19 | t.Error("Couldn't make UDP listener: ", err)
20 | }
21 |
22 | rolodex, err := NewRolodex(conn, time.Second, time.Minute)
23 |
24 | if err != nil {
25 | t.Error("Couldn't make rolodex: ", err)
26 | }
27 |
28 | go rolodex.Run()
29 |
30 | // use these as fake tuns
31 | // the incoming is the tun to the outside world (ie what other applications would write to)
32 | // and the outgoing is what meshboi reads and writes to
33 | tunIncoming1, tunOutgoing1 := net.Pipe()
34 | tunIncoming2, tunOutgoing2 := net.Pipe()
35 |
36 | client1, err := NewMeshBoiClient(tunOutgoing1, netaddr.MustParseIPPrefix("192.168.52.1/24"), netaddr.MustParseIP("127.0.0.1"), 12345, "testNetwork", []byte("testpassword"))
37 |
38 | if err != nil {
39 | t.Error("Error making mesh client ", err)
40 | }
41 |
42 | client2, err := NewMeshBoiClient(tunOutgoing2, netaddr.MustParseIPPrefix("192.168.52.2/24"), netaddr.MustParseIP("127.0.0.1"), 12345, "testNetwork", []byte("testpassword"))
43 |
44 | if err != nil {
45 | t.Error("Error making mesh client ", err)
46 | }
47 |
48 | go client1.Run()
49 | defer client1.Stop()
50 |
51 | go client2.Run()
52 | defer client2.Stop()
53 |
54 | b := []byte("hello how are you?")
55 | h := &ipv4.Header{
56 | Version: ipv4.Version,
57 | Len: ipv4.HeaderLen,
58 | TotalLen: ipv4.HeaderLen + len(b),
59 | ID: 55555,
60 | Protocol: 1,
61 | Dst: net.ParseIP("192.168.52.2"),
62 | }
63 |
64 | header, err := h.Marshal()
65 |
66 | if err != nil {
67 | t.Error("Error marshalling header ", err)
68 | }
69 |
70 | sentMsg := append(header, b...)
71 |
72 | // The connection between the peers takes at least 1 second to create
73 | //
74 | // todo: It would be much nicer if we could get the client to inform us when
75 | // the connection has been made so we could wait on a condition var or channel
76 | // instead of needing to sleep here
77 | time.Sleep(2 * time.Second)
78 | tunIncoming1.Write(sentMsg)
79 |
80 | var rxedMsg []byte
81 |
82 | rxedMsg = make([]byte, 4000)
83 |
84 | n, err := tunIncoming2.Read(rxedMsg)
85 |
86 | if err != nil {
87 | t.Error("Error reading from tun ", err)
88 | }
89 |
90 | if !reflect.DeepEqual(rxedMsg[:n], sentMsg) {
91 | t.Errorf("Didn't read expected data %v %v", rxedMsg[:n], sentMsg)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/peer_conn_store_test.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "bytes"
5 | "net"
6 | "testing"
7 |
8 | "inet.af/netaddr"
9 | )
10 |
11 | type FakeTun struct {
12 | bytes.Buffer
13 | }
14 |
15 | func (f *FakeTun) Close() error {
16 | return nil
17 | }
18 |
19 | func NewFakePeerConn(inside string, outside string) *PeerConn {
20 | insideIP := netaddr.MustParseIP(inside)
21 | outsideIP := netaddr.MustParseIPPort(outside)
22 | _, client := net.Pipe()
23 | p := NewPeerConn(insideIP, outsideIP, client, &FakeTun{})
24 | return &p
25 | }
26 |
27 | type test struct {
28 | insideIP string
29 | outsideIP string
30 | }
31 |
32 | var tests = []test{
33 | {insideIP: "192.168.4.1", outsideIP: "192.168.44.1:5000"},
34 | {insideIP: "10.0.0.1", outsideIP: "1.1.1.1:2334"},
35 | {insideIP: "2.2.2.2", outsideIP: "3.4.3.3:2"},
36 | }
37 |
38 | func TestGetByIP(t *testing.T) {
39 | store := NewPeerConnStore()
40 |
41 | for _, tc := range tests {
42 | pc := NewFakePeerConn(tc.insideIP, tc.outsideIP)
43 | store.Add(pc)
44 |
45 | retrievedPeerConn, ok := store.GetByInsideIp(netaddr.MustParseIP(tc.insideIP))
46 |
47 | if !ok {
48 | t.Errorf("Couldn't find peer conn by inside IP")
49 | }
50 |
51 | if retrievedPeerConn != pc {
52 | t.Errorf("Wrong peer conn returned for inside IP")
53 | }
54 |
55 | retrievedPeerConn, ok = store.GetByOutsideIpPort(netaddr.MustParseIPPort(tc.outsideIP))
56 |
57 | if !ok {
58 | t.Errorf("Couldn't find peer conn by outside IP")
59 | }
60 |
61 | if retrievedPeerConn != pc {
62 | t.Errorf("Wrong peer conn returned for outside IP")
63 | }
64 | }
65 | }
66 |
67 | func TestGetNotExisting(t *testing.T) {
68 | store := NewPeerConnStore()
69 |
70 | _, ok := store.GetByInsideIp(netaddr.MustParseIP("192.168.1.1"))
71 |
72 | if ok {
73 | t.Errorf("Shouldn't have gotten a peer conn back")
74 | }
75 |
76 | _, ok = store.GetByOutsideIpPort(netaddr.MustParseIPPort("192.168.1.1:5000"))
77 |
78 | if ok {
79 | t.Errorf("Shouldn't have gotten a peer conn back")
80 | }
81 | }
82 |
83 | func TestDeleteByIP(t *testing.T) {
84 | store := NewPeerConnStore()
85 |
86 | pc := NewFakePeerConn(tests[0].insideIP, tests[0].outsideIP)
87 | store.Add(pc)
88 |
89 | ok := store.RemoveByOutsideIPPort(netaddr.MustParseIPPort(tests[0].outsideIP))
90 |
91 | if !ok {
92 | t.Errorf("Could not remove peer conn")
93 | }
94 |
95 | _, ok = store.GetByInsideIp(netaddr.MustParseIP(tests[0].insideIP))
96 |
97 | if ok {
98 | t.Errorf("Found deleted peer")
99 | }
100 |
101 | _, ok = store.GetByOutsideIpPort(netaddr.MustParseIPPort(tests[0].outsideIP))
102 |
103 | if ok {
104 | t.Errorf("Found deleted peer")
105 | }
106 |
107 | }
108 |
109 | func TestDeleteNonExistentIP(t *testing.T) {
110 | store := NewPeerConnStore()
111 |
112 | ok := store.RemoveByOutsideIPPort(netaddr.MustParseIPPort(tests[0].outsideIP))
113 |
114 | if ok {
115 | t.Errorf("Deleting a non existing IP Port shouldn't work")
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/multiplexed_dtls_conn.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/pion/dtls/v2"
7 | "github.com/pion/dtls/v2/pkg/protocol"
8 | "github.com/pion/dtls/v2/pkg/protocol/recordlayer"
9 | "github.com/samvrlewis/udp"
10 | log "github.com/sirupsen/logrus"
11 | "inet.af/netaddr"
12 | )
13 |
14 | // VpnListenerDialer allows for:
15 | // - Dialing connections to other members in the VPN Mesh
16 | // - Accepting connections to other members in the VPN Mesh
17 | // - Dialing connections to non VPN Mesh members
18 | type VpnMeshListenerDialer interface {
19 | // Returns the connection and the VPN IP address on the other side
20 | AcceptMesh() (MeshConn, error)
21 | // Returns the connection and the VPN IP address on the other side
22 | DialMesh(raddr net.Addr) (MeshConn, error)
23 | Dial(raddr net.Addr) (net.Conn, error)
24 | }
25 |
26 | type MeshConn interface {
27 | net.Conn
28 | RemoteMeshAddr() netaddr.IP
29 | }
30 |
31 | type meshConn struct {
32 | net.Conn
33 | remoteMeshAddr netaddr.IP
34 | }
35 |
36 | func (m *meshConn) RemoteMeshAddr() netaddr.IP {
37 | return m.remoteMeshAddr
38 | }
39 |
40 | // MultiplexedDTLSConn represents a conn that can be used to listen for new incoming DTLS connections
41 | // and also dial new UDP connections (both DTLS and non-DTLS) from the same udp address
42 | type MultiplexedDTLSConn struct {
43 | listener *udp.Listener
44 | config *dtls.Config
45 | }
46 |
47 | func NewMultiplexedDTLSConn(laddr *net.UDPAddr, config *dtls.Config) (*MultiplexedDTLSConn, error) {
48 | // Set a listen config so that we only accept incoming connections that are DTLS connections
49 | lc := udp.ListenConfig{
50 | AcceptFilter: func(packet []byte) bool {
51 | pkts, err := recordlayer.UnpackDatagram(packet)
52 | if err != nil || len(pkts) < 1 {
53 | return false
54 | }
55 | h := &recordlayer.Header{}
56 | if err := h.Unmarshal(pkts[0]); err != nil {
57 | return false
58 | }
59 | return h.ContentType == protocol.ContentTypeHandshake
60 | },
61 | }
62 |
63 | listener, err := lc.Listen("udp", laddr)
64 |
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | return &MultiplexedDTLSConn{
70 | listener: listener.(*udp.Listener),
71 | config: config,
72 | }, nil
73 | }
74 |
75 | func (mc *MultiplexedDTLSConn) startDtlsConn(conn net.Conn, isServer bool) (MeshConn, error) {
76 | var dtlsConn *dtls.Conn
77 | var err error
78 |
79 | if isServer {
80 | dtlsConn, err = dtls.Server(conn, mc.config)
81 | } else {
82 | dtlsConn, err = dtls.Client(conn, mc.config)
83 | }
84 |
85 | if err != nil {
86 | log.Warn("Error starting dtls connection: ", err)
87 | conn.Close()
88 | return nil, err
89 | }
90 |
91 | peerIpString := string(dtlsConn.ConnectionState().IdentityHint)
92 | peerVpnIP, err := netaddr.ParseIP(peerIpString)
93 |
94 | if err != nil {
95 | log.Warn("Couldn't parse peers vpn IP")
96 | return nil, err
97 | }
98 |
99 | return &meshConn{Conn: dtlsConn,
100 | remoteMeshAddr: peerVpnIP,
101 | }, nil
102 | }
103 |
104 | func (mc *MultiplexedDTLSConn) AcceptMesh() (MeshConn, error) {
105 | conn, err := mc.listener.Accept()
106 |
107 | if err != nil {
108 | return nil, err
109 | }
110 |
111 | return mc.startDtlsConn(conn, true)
112 |
113 | }
114 |
115 | func (mc *MultiplexedDTLSConn) DialMesh(raddr net.Addr) (MeshConn, error) {
116 | conn, err := mc.listener.Dial(raddr)
117 |
118 | if err != nil {
119 | return nil, err
120 | }
121 |
122 | return mc.startDtlsConn(conn, false)
123 | }
124 |
125 | func (mc *MultiplexedDTLSConn) Dial(raddr net.Addr) (net.Conn, error) {
126 | return mc.listener.Dial(raddr)
127 | }
128 |
--------------------------------------------------------------------------------
/cmd/meshboi/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "net"
7 | "os"
8 | "os/signal"
9 | "time"
10 |
11 | "github.com/samvrlewis/meshboi"
12 | log "github.com/sirupsen/logrus"
13 | "inet.af/netaddr"
14 | )
15 |
16 | const defaultPort = 6264 // "mboi" on a telelphone dialpad :)
17 |
18 | const usage = `usage: meshboi args
19 |
20 | Command can be one of
21 |
22 | rolodex Start meshboi in rollodex mode
23 | client Join as client in an existing peer to peer mesh
24 |
25 | More information on both commands and the arguments needed can be found with
26 | meshboi -help. (eg: meshboi rolodex -help).`
27 |
28 | func printUsage() {
29 | fmt.Println(usage)
30 | os.Exit(1)
31 | }
32 |
33 | func main() {
34 |
35 | rolodexCommand := flag.NewFlagSet("rolodex", flag.ExitOnError)
36 | ip := rolodexCommand.String("listen-address", "0.0.0.0", "The IP address for the rolodex to listen on")
37 | port := rolodexCommand.Int("listen-port", defaultPort, "The port of for the rolodex to listen on")
38 |
39 | clientCommand := flag.NewFlagSet("client", flag.ExitOnError)
40 | networkName := clientCommand.String("network", "", "The unique network name that identifies the mesh (should be the same on all members in the mesh)")
41 | tunName := clientCommand.String("tun-name", "tun", "The name to assign to the tun adapter")
42 | tunMtu := clientCommand.Int("tun-mtu", 1200, "The MTU of the tun")
43 | vpnIPPrefixString := clientCommand.String("vpn-ip", "", "The IP address (with subnet) to assign to the tunnel eg: 192.168.50.1/24")
44 | rolodexAddr := clientCommand.String("rolodex-address", "rolodex.samlewis.me", "The IP address of the meshboi server")
45 | rolodexPort := clientCommand.Int("rolodex-port", defaultPort, "The port of the server")
46 | psk := clientCommand.String("psk", "", "The pre shared key to use (should be the same on all members in the mesh)")
47 |
48 | if len(os.Args) < 2 {
49 | printUsage()
50 | }
51 |
52 | switch os.Args[1] {
53 | case "rolodex":
54 | rolodexCommand.Parse(os.Args[2:])
55 | case "client":
56 | clientCommand.Parse(os.Args[2:])
57 | default:
58 | printUsage()
59 | }
60 |
61 | if clientCommand.Parsed() {
62 | if *psk == "" {
63 | log.Error("psk argument not set. Please set with a secure password")
64 | clientCommand.PrintDefaults()
65 | os.Exit(1)
66 | }
67 |
68 | if *networkName == "" {
69 | log.Error("network argument not set.")
70 | clientCommand.PrintDefaults()
71 | os.Exit(1)
72 | }
73 |
74 | if *vpnIPPrefixString == "" {
75 | log.Error("vpn-ip argument not set.")
76 | clientCommand.PrintDefaults()
77 | os.Exit(1)
78 | }
79 |
80 | vpnIPPrefix, err := netaddr.ParseIPPrefix(*vpnIPPrefixString)
81 |
82 | tun, err := meshboi.NewTunWithConfig(*tunName, vpnIPPrefix.String(), *tunMtu)
83 |
84 | if err != nil {
85 | log.Fatalln("Error creating tun: ", err)
86 | }
87 |
88 | rolodexStdIP, err := net.ResolveIPAddr("ip", *rolodexAddr)
89 |
90 | if err != nil {
91 | log.Fatalln("Error parsing rolodex-address ", err)
92 | }
93 |
94 | rolodexIP, ok := netaddr.FromStdIP(rolodexStdIP.IP)
95 |
96 | if !ok {
97 | log.Fatalln("Error converting to netaddr IP")
98 | }
99 |
100 | mc, err := meshboi.NewMeshBoiClient(tun, vpnIPPrefix, rolodexIP, *rolodexPort, *networkName, []byte(*psk))
101 |
102 | if err != nil {
103 | log.Fatalln("Error starting mesh client ", err)
104 | }
105 |
106 | mc.Run()
107 | } else if rolodexCommand.Parsed() {
108 | addr := &net.UDPAddr{IP: net.ParseIP(*ip), Port: *port}
109 | conn, err := net.ListenUDP("udp", addr)
110 |
111 | if err != nil {
112 | log.Fatalln("Error starting listener ", err)
113 | }
114 |
115 | rollo, err := meshboi.NewRolodex(conn, 5*time.Second, 30*time.Second)
116 |
117 | if err != nil {
118 | log.Fatalln("Error creating rolodex ", err)
119 | }
120 | rollo.Run()
121 | }
122 |
123 | c := make(chan os.Signal)
124 | signal.Notify(c, os.Interrupt)
125 | select {
126 | case <-c:
127 | log.Info("Shutting down")
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/rolodex.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "encoding/json"
5 | "net"
6 | "sync"
7 | "time"
8 |
9 | log "github.com/sirupsen/logrus"
10 | "inet.af/netaddr"
11 | )
12 |
13 | type rolodex struct {
14 | conn *net.UDPConn
15 | networks map[string]*meshNetwork
16 | sendInterval time.Duration
17 | timeOutDuration time.Duration
18 | }
19 |
20 | const TimeOutSecs = 30
21 |
22 | type meshNetwork struct {
23 | // make of IP address to last seen time
24 | members map[netaddr.IPPort]time.Time
25 | membersLock sync.RWMutex
26 | rollo *rolodex
27 | name string
28 | newMember chan struct{}
29 | }
30 |
31 | func (m *meshNetwork) register(addr netaddr.IPPort) {
32 | m.membersLock.Lock()
33 | _, ok := m.members[addr]
34 | m.members[addr] = time.Now()
35 | m.membersLock.Unlock()
36 |
37 | if !ok {
38 | log.WithFields(log.Fields{
39 | "address": addr,
40 | "name": m.name,
41 | }).Info("Registering new mesh member")
42 | m.newMember <- struct{}{}
43 | }
44 | }
45 |
46 | func (r *rolodex) getNetwork(networkName string) *meshNetwork {
47 | if network, ok := r.networks[networkName]; ok {
48 | return network
49 | }
50 |
51 | network := &meshNetwork{}
52 | network.members = make(map[netaddr.IPPort]time.Time)
53 | network.rollo = r
54 | network.newMember = make(chan struct{})
55 | network.name = networkName
56 | r.networks[networkName] = network
57 |
58 | go network.Serve()
59 |
60 | return network
61 | }
62 |
63 | func NewRolodex(conn *net.UDPConn, sendInterval time.Duration, timeOutDuration time.Duration) (*rolodex, error) {
64 | rollo := &rolodex{}
65 | rollo.conn = conn
66 | rollo.sendInterval = sendInterval
67 | rollo.timeOutDuration = timeOutDuration
68 | rollo.networks = make(map[string]*meshNetwork)
69 |
70 | return rollo, nil
71 | }
72 |
73 | func (r *rolodex) Run() {
74 | buf := make([]byte, 65535)
75 | for {
76 | n, addr, err := r.conn.ReadFromUDP(buf)
77 |
78 | if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
79 | log.Warn("Temporary error reading data: ", nerr)
80 | continue
81 | }
82 |
83 | var message HeartbeatMessage
84 |
85 | if err := json.Unmarshal(buf[:n], &message); err != nil {
86 | log.Error("Error unmarshalling ", err)
87 | continue
88 | }
89 |
90 | mesh := r.getNetwork(message.NetworkName)
91 | ipPort, ok := netaddr.FromStdAddr(addr.IP, addr.Port, "")
92 |
93 | if !ok {
94 | log.Error("Error converting to netaddr ", err)
95 | continue
96 | }
97 |
98 | mesh.register(ipPort)
99 | }
100 | }
101 |
102 | func (mesh *meshNetwork) timeOutInactiveMembers() {
103 | mesh.membersLock.Lock()
104 | defer mesh.membersLock.Unlock()
105 |
106 | now := time.Now()
107 |
108 | for member := range mesh.members {
109 | timeSinceLastActive := now.Sub(mesh.members[member])
110 |
111 | if timeSinceLastActive > mesh.rollo.timeOutDuration {
112 | log.WithFields(log.Fields{
113 | "address": member.IP,
114 | }).Info("Removing member due to timeout")
115 | delete(mesh.members, member)
116 | }
117 | }
118 | }
119 |
120 | // Serve sends out messages to each member so that they're aware of other members they can connect to
121 | // It also serves as a heart beat of sorts from the rolodex to the member
122 | func (mesh *meshNetwork) Serve() {
123 | ticker := time.NewTicker(mesh.rollo.sendInterval)
124 | quit := make(chan int)
125 | for {
126 | // Send out an update both periodically, and on the event of a new member joining
127 | select {
128 | case <-ticker.C:
129 | break
130 | case <-mesh.newMember:
131 | break
132 | case <-quit:
133 | ticker.Stop()
134 | return
135 | }
136 |
137 | // reset the ticker in case we're sending an update due to a new member joining
138 | ticker.Reset(mesh.rollo.sendInterval)
139 |
140 | mesh.timeOutInactiveMembers()
141 |
142 | mesh.membersLock.RLock()
143 | memberIps := make([]netaddr.IPPort, 0, len(mesh.members))
144 | for member := range mesh.members {
145 | memberIps = append(memberIps, member)
146 | }
147 |
148 | memberMessage := NetworkMap{Addresses: memberIps}
149 | memberMessage.YourIndex = 0
150 |
151 | for _, member := range memberIps {
152 | b, err := json.Marshal(memberMessage)
153 | if err != nil {
154 | panic(err)
155 | }
156 | mesh.rollo.conn.WriteToUDP(b, member.UDPAddr())
157 | memberMessage.YourIndex += 1
158 | }
159 | mesh.membersLock.RUnlock()
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/peer_connector.go:
--------------------------------------------------------------------------------
1 | package meshboi
2 |
3 | import (
4 | "net"
5 | "time"
6 |
7 | "inet.af/netaddr"
8 |
9 | log "github.com/sirupsen/logrus"
10 | )
11 |
12 | type PeerConnector struct {
13 | store *PeerConnStore
14 | listenerDialer VpnMeshListenerDialer
15 | myOutsideAddr netaddr.IPPort
16 | tun TunConn
17 | }
18 |
19 | // Simple comparison to see if this member should be the server or if the remote member should be
20 | func (pc *PeerConnector) AmServer(other netaddr.IPPort) bool {
21 | ipCompare := pc.myOutsideAddr.IP.Compare(other.IP)
22 |
23 | switch ipCompare {
24 | case -1:
25 | return false
26 | case 0:
27 | if pc.myOutsideAddr.Port > other.Port {
28 | return true
29 | } else if pc.myOutsideAddr.Port < other.Port {
30 | return false
31 | } else {
32 | panic("Remote IPPort == Local IPPort")
33 | }
34 | case 1:
35 | return true
36 | default:
37 | panic("Unexpected comparison result")
38 | }
39 | }
40 |
41 | func NewPeerConnector(listenerDialer VpnMeshListenerDialer, store *PeerConnStore, tun TunConn) PeerConnector {
42 | return PeerConnector{
43 | listenerDialer: listenerDialer,
44 | store: store,
45 | tun: tun,
46 | }
47 | }
48 |
49 | func (pc *PeerConnector) OnNetworkMapUpdate(network NetworkMap) {
50 | pc.myOutsideAddr = network.Addresses[network.YourIndex]
51 | pc.newAddresses(network.Addresses)
52 | }
53 |
54 | func (pc *PeerConnector) readAllFromAddr(address net.Addr, timeout time.Duration) error {
55 | conn, err := pc.listenerDialer.Dial(address)
56 |
57 | if err != nil {
58 | log.Warn("Error connecting to peer unencrypted ", err)
59 | return err
60 | }
61 |
62 | defer conn.Close()
63 |
64 | buf := make([]byte, 1024)
65 |
66 | conn.SetReadDeadline(time.Now().Add(timeout))
67 | n, err := conn.Read(buf)
68 |
69 | if err != nil {
70 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
71 | log.Println("Read timeout:", err)
72 | } else {
73 | log.Println("Read error:", err)
74 | return err
75 | }
76 | } else {
77 | log.Info("Read: ", string(buf[:n]))
78 | }
79 |
80 | return nil
81 | }
82 |
83 | func (pc *PeerConnector) connectToNewPeer(address netaddr.IPPort) error {
84 | // We are going to initiate a dTLS connection to the other mesh member
85 | // however, for it to open a hole in its firewall it has sent us an initial
86 | // message if we have our own firewall then this message will likely not be
87 | // received but if it does get through it can wait here to receive it before
88 | // continuing
89 | err := pc.readAllFromAddr(address.UDPAddr(), time.Second)
90 |
91 | if err != nil {
92 | return err
93 | }
94 |
95 | conn, err := pc.listenerDialer.DialMesh(address.UDPAddr())
96 |
97 | if err != nil {
98 | return err
99 | }
100 |
101 | return pc.OnNewPeerConnection(conn)
102 | }
103 |
104 | func (pc *PeerConnector) OnNewPeerConnection(conn MeshConn) error {
105 | outsideAddr, err := netaddr.ParseIPPort(conn.RemoteAddr().String())
106 |
107 | if err != nil {
108 | conn.Close()
109 | return err
110 | }
111 |
112 | log.Info("Succesfully accepted connection from ", conn.RemoteAddr())
113 |
114 | peer := NewPeerConn(conn.RemoteMeshAddr(), outsideAddr, conn, pc.tun)
115 |
116 | pc.store.Add(&peer)
117 |
118 | go peer.readLoop()
119 | go peer.sendLoop()
120 |
121 | return nil
122 | }
123 |
124 | func (pc *PeerConnector) openFirewallToPeer(addr net.Addr) error {
125 | conn, err := pc.listenerDialer.Dial(addr)
126 | defer conn.Close()
127 |
128 | if err != nil {
129 | log.Warn("Error connecting to peer unencrypted ", err)
130 | return err
131 | }
132 |
133 | // It doesn't really matter what is sent here - the important part is
134 | // something is sent. We're effectively telling any and all (stateful)
135 | // firewalls on our path to the peer to allow any future traffic that has
136 | // originated from that peer
137 | _, err = conn.Write([]byte("hello"))
138 |
139 | if err != nil {
140 | log.Warn("Error writing to peer unencrypted ", err)
141 | return err
142 | }
143 |
144 | return nil
145 | }
146 |
147 | func (pc *PeerConnector) newAddresses(addreses []netaddr.IPPort) {
148 | for _, address := range addreses {
149 | _, ok := pc.store.GetByOutsideIpPort(address)
150 |
151 | if ok {
152 | // we already know of this peer
153 | continue
154 | }
155 |
156 | if address == pc.myOutsideAddr {
157 | // don't connect to myself
158 | continue
159 | }
160 |
161 | if pc.AmServer(address) {
162 | peer := NewPeerConn(netaddr.IP{}, address, nil, pc.tun)
163 | pc.store.Add(&peer)
164 |
165 | // As the peer will initiate connection to our dTLS server we first
166 | // need to make sure our firewall(s) are open to allow the peer to
167 | // contact us
168 | err := pc.openFirewallToPeer(address.UDPAddr())
169 |
170 | if err != nil {
171 | log.Warn("Error opening firewall: ", err)
172 |
173 | // Remove the peer so we can try again later
174 | pc.store.RemoveByOutsideIPPort(address)
175 | }
176 | } else {
177 | log.Info("Going to try to connect to ", address)
178 |
179 | if err := pc.connectToNewPeer(address); err != nil {
180 | log.Warn("Could not connect to ", address, err)
181 | continue
182 | }
183 | }
184 | }
185 | }
186 |
187 | func (pc *PeerConnector) ListenForPeers() {
188 | for {
189 | conn, err := pc.listenerDialer.AcceptMesh()
190 |
191 | if err != nil {
192 | log.Warn("Error accepting: ", err)
193 | continue
194 | }
195 |
196 | pc.OnNewPeerConnection(conn)
197 | }
198 | }
199 |
200 | func (pc *PeerConnector) Stop() {
201 | // todo: Properly shut down all the spawned peers
202 | }
203 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
6 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
9 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
11 | github.com/pion/dtls/v2 v2.0.8 h1:reGe8rNIMfO/UAeFLqO61tl64t154Qfkr4U3Gzu1tsg=
12 | github.com/pion/dtls/v2 v2.0.8/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10=
13 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
14 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
15 | github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
16 | github.com/pion/transport v0.12.2 h1:WYEjhloRHt1R86LhUKjC5y+P52Y11/QqEUalvtzVoys=
17 | github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
18 | github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
19 | github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
22 | github.com/samvrlewis/udp v0.1.1-0.20210505081938-3a6139185318 h1:Yf3E9FPAXo4IvclHR+bL0pSAXSD21f9b2zGGXiQgEX8=
23 | github.com/samvrlewis/udp v0.1.1-0.20210505081938-3a6139185318/go.mod h1:uPAJz0sRZjW96FFMfL6nGQNqDJNpfgz0cIOy0tF2yB8=
24 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
25 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
27 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
28 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
29 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
30 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
31 | go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
32 | go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
33 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
34 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
35 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
37 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
38 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
39 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
40 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
41 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
42 | golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
43 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
44 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
45 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
46 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
47 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
48 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
49 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
50 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
51 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
52 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
53 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
54 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
55 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
56 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
57 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
60 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
61 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
62 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
63 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
64 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
65 | inet.af/netaddr v0.0.0-20210313195008-843b4240e319 h1:cSGjHEjS/Nu6pts6yZfSYsRqcfW5ieqSrQI+XcZQObM=
66 | inet.af/netaddr v0.0.0-20210313195008-843b4240e319/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4=
67 |
--------------------------------------------------------------------------------