├── 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 | # ![logo](https://user-images.githubusercontent.com/3880246/124463187-dd916d00-ddd5-11eb-8e0a-923629365637.png) 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 | [![asciinema](https://user-images.githubusercontent.com/3880246/124463198-e124f400-ddd5-11eb-94e9-23de8797137f.png)](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 | --------------------------------------------------------------------------------