From 0ae0f31eae0e270413cd8256eb12cae515e2db8f Mon Sep 17 00:00:00 2001 From: jdl Date: Thu, 19 Dec 2024 20:53:52 +0100 Subject: [PATCH] wip --- node/cipher-data.go | 61 ++++++++++ ...datacipher_test.go => cipher-data_test.go} | 28 +++-- node/cipher-routing.go | 26 ++++ node/cipher-routing_test.go | 114 ++++++++++++++++++ node/cipher.go | 6 + node/conn.go | 39 +++++- node/datacipher.go | 97 --------------- node/header.go | 35 ++++++ node/packets.go | 89 ++++++++++++++ node/packets_test.go | 42 +++++++ node/peersupervisor.go | 14 +-- node/router.go | 18 ++- node/routingpacket.go | 9 -- 13 files changed, 443 insertions(+), 135 deletions(-) create mode 100644 node/cipher-data.go rename node/{datacipher_test.go => cipher-data_test.go} (78%) create mode 100644 node/cipher-routing.go create mode 100644 node/cipher-routing_test.go create mode 100644 node/cipher.go delete mode 100644 node/datacipher.go create mode 100644 node/packets.go create mode 100644 node/packets_test.go diff --git a/node/cipher-data.go b/node/cipher-data.go new file mode 100644 index 0000000..c0fc273 --- /dev/null +++ b/node/cipher-data.go @@ -0,0 +1,61 @@ +package node + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" +) + +type dataCipher struct { + key []byte + aead cipher.AEAD +} + +func newDataCipher() *dataCipher { + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + panic(err) + } + return newDataCipherFromKey(key) +} + +// key must be 32 bytes. +func newDataCipherFromKey(key []byte) *dataCipher { + block, err := aes.NewCipher(key) + if err != nil { + panic(err) + } + + aead, err := cipher.NewGCM(block) + if err != nil { + panic(err) + } + + return &dataCipher{key: key, aead: aead} +} + +func (sc *dataCipher) Key() []byte { + return sc.key +} + +func (sc *dataCipher) Encrypt(h xHeader, data, out []byte) []byte { + const s = dataHeaderSize + out = out[:s+dataCipherOverhead+len(data)] + h.Marshal(dataStreamID, out[:s]) + sc.aead.Seal(out[s:s], out[:s], data, nil) + return out +} + +func (sc *dataCipher) Decrypt(encrypted, out []byte) (data []byte, ok bool) { + const s = dataHeaderSize + if len(encrypted) < s+dataCipherOverhead { + ok = false + return + } + + var err error + + data, err = sc.aead.Open(out[:0], encrypted[:s], encrypted[s:], nil) + ok = err == nil + return +} diff --git a/node/datacipher_test.go b/node/cipher-data_test.go similarity index 78% rename from node/datacipher_test.go rename to node/cipher-data_test.go index 8a0f012..d1523d8 100644 --- a/node/datacipher_test.go +++ b/node/cipher-data_test.go @@ -22,7 +22,7 @@ func TestDataCipher(t *testing.T) { } for _, plaintext := range testCases { - h1 := dataHeader{ + h1 := xHeader{ Counter: 235153, SourceIP: 4, DestIP: 88, @@ -31,11 +31,13 @@ func TestDataCipher(t *testing.T) { encrypted := make([]byte, bufferSize) dc1 := newDataCipher() - encrypted = dc1.Encrypt(&h1, plaintext, encrypted) + encrypted = dc1.Encrypt(h1, plaintext, encrypted) + h2 := xHeader{} + h2.Parse(encrypted) dc2 := newDataCipherFromKey(dc1.Key()) - decrypted, h2, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize)) + decrypted, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize)) if !ok { t.Fatal(ok) } @@ -64,7 +66,7 @@ func TestDataCipher_ModifyCiphertext(t *testing.T) { } for _, plaintext := range testCases { - h1 := dataHeader{ + h1 := xHeader{ Counter: 235153, SourceIP: 4, DestIP: 88, @@ -73,14 +75,14 @@ func TestDataCipher_ModifyCiphertext(t *testing.T) { encrypted := make([]byte, bufferSize) dc1 := newDataCipher() - encrypted = dc1.Encrypt(&h1, plaintext, encrypted) + encrypted = dc1.Encrypt(h1, plaintext, encrypted) encrypted[mrand.IntN(len(encrypted))]++ dc2 := newDataCipherFromKey(dc1.Key()) - _, h2, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize)) + _, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize)) if ok { - t.Fatal(ok, h2) + t.Fatal(ok) } } } @@ -89,14 +91,14 @@ func TestDataCipher_ShortCiphertext(t *testing.T) { dc1 := newDataCipher() shortText := make([]byte, dataHeaderSize+dataCipherOverhead-1) rand.Read(shortText) - _, _, ok := dc1.Decrypt(shortText, make([]byte, bufferSize)) + _, ok := dc1.Decrypt(shortText, make([]byte, bufferSize)) if ok { t.Fatal(ok) } } func BenchmarkDataCipher_Encrypt(b *testing.B) { - h1 := dataHeader{ + h1 := xHeader{ Counter: 235153, SourceIP: 4, DestIP: 88, @@ -110,12 +112,12 @@ func BenchmarkDataCipher_Encrypt(b *testing.B) { dc1 := newDataCipher() b.ResetTimer() for i := 0; i < b.N; i++ { - encrypted = dc1.Encrypt(&h1, plaintext, encrypted) + encrypted = dc1.Encrypt(h1, plaintext, encrypted) } } func BenchmarkDataCipher_Decrypt(b *testing.B) { - h1 := dataHeader{ + h1 := xHeader{ Counter: 235153, SourceIP: 4, DestIP: 88, @@ -127,12 +129,12 @@ func BenchmarkDataCipher_Decrypt(b *testing.B) { encrypted := make([]byte, bufferSize) dc1 := newDataCipher() - encrypted = dc1.Encrypt(&h1, plaintext, encrypted) + encrypted = dc1.Encrypt(h1, plaintext, encrypted) decrypted := make([]byte, bufferSize) b.ResetTimer() for i := 0; i < b.N; i++ { - decrypted, _, _ = dc1.Decrypt(encrypted, decrypted) + decrypted, _ = dc1.Decrypt(encrypted, decrypted) } } diff --git a/node/cipher-routing.go b/node/cipher-routing.go new file mode 100644 index 0000000..795ac7a --- /dev/null +++ b/node/cipher-routing.go @@ -0,0 +1,26 @@ +package node + +import "golang.org/x/crypto/nacl/box" + +type routingCipher struct { + sharedKey [32]byte +} + +func newRoutingCipher(privKey, pubKey []byte) routingCipher { + shared := [32]byte{} + box.Precompute(&shared, (*[32]byte)(pubKey), (*[32]byte)(privKey)) + return routingCipher{shared} +} + +func (rc routingCipher) Encrypt(h xHeader, data, out []byte) []byte { + const s = routingHeaderSize + out = out[:s+routingCipherOverhead+len(data)] + h.Marshal(routingStreamID, out[:s]) + box.SealAfterPrecomputation(out[s:s], data, (*[24]byte)(out[:s]), &rc.sharedKey) + return out +} + +func (rc routingCipher) Decrypt(encrypted, out []byte) (data []byte, ok bool) { + const s = routingHeaderSize + return box.OpenAfterPrecomputation(out[:0], encrypted[s:], (*[24]byte)(encrypted[:s]), &rc.sharedKey) +} diff --git a/node/cipher-routing_test.go b/node/cipher-routing_test.go new file mode 100644 index 0000000..09824f7 --- /dev/null +++ b/node/cipher-routing_test.go @@ -0,0 +1,114 @@ +package node + +import ( + "bytes" + "crypto/rand" + "testing" + + "golang.org/x/crypto/nacl/box" +) + +func newRoutingCipherForTesting() (c1, c2 routingCipher) { + pubKey1, privKey1, err := box.GenerateKey(rand.Reader) + if err != nil { + panic(err) + } + + pubKey2, privKey2, err := box.GenerateKey(rand.Reader) + if err != nil { + panic(err) + } + + return newRoutingCipher(privKey1[:], pubKey2[:]), + newRoutingCipher(privKey2[:], pubKey1[:]) +} + +func TestRoutingCipher(t *testing.T) { + c1, c2 := newRoutingCipherForTesting() + + maxSizePlaintext := make([]byte, bufferSize-routingHeaderSize-routingCipherOverhead) + rand.Read(maxSizePlaintext) + + testCases := [][]byte{ + make([]byte, 0), + {1}, + {255}, + {1, 2, 3, 4, 5}, + []byte("Hello world"), + maxSizePlaintext, + } + + for _, plaintext := range testCases { + h1 := xHeader{ + Counter: 235153, + SourceIP: 4, + DestIP: 88, + } + + encrypted := make([]byte, bufferSize) + + encrypted = c1.Encrypt(h1, plaintext, encrypted) + + decrypted, ok := c2.Decrypt(encrypted, make([]byte, bufferSize)) + if !ok { + t.Fatal(ok) + } + + if !bytes.Equal(decrypted, plaintext) { + t.Fatal("not equal") + } + } +} + +func TestRoutingCipher_ShortCiphertext(t *testing.T) { + c1, _ := newRoutingCipherForTesting() + shortText := make([]byte, routingHeaderSize+routingCipherOverhead-1) + rand.Read(shortText) + _, ok := c1.Decrypt(shortText, make([]byte, bufferSize)) + if ok { + t.Fatal(ok) + } +} + +func BenchmarkRoutingCipher_Encrypt(b *testing.B) { + c1, _ := newRoutingCipherForTesting() + h1 := xHeader{ + Counter: 235153, + SourceIP: 4, + DestIP: 88, + } + + plaintext := make([]byte, bufferSize-routingHeaderSize-routingCipherOverhead) + rand.Read(plaintext) + + encrypted := make([]byte, bufferSize) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + encrypted = c1.Encrypt(h1, plaintext, encrypted) + } +} + +func BenchmarkRoutingCipher_Decrypt(b *testing.B) { + c1, c2 := newRoutingCipherForTesting() + + h1 := xHeader{ + Counter: 235153, + SourceIP: 4, + DestIP: 88, + } + + plaintext := make([]byte, bufferSize-routingHeaderSize-routingCipherOverhead) + rand.Read(plaintext) + + encrypted := make([]byte, bufferSize) + + encrypted = c1.Encrypt(h1, plaintext, encrypted) + + decrypted := make([]byte, bufferSize) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + decrypted, _ = c2.Decrypt(encrypted, decrypted) + } +} diff --git a/node/cipher.go b/node/cipher.go new file mode 100644 index 0000000..cb7accd --- /dev/null +++ b/node/cipher.go @@ -0,0 +1,6 @@ +package node + +type packetCipher interface { + Encrypt(h xHeader, data, out []byte) []byte + Decrypt(encrypted, out []byte) (data []byte, ok bool) +} diff --git a/node/conn.go b/node/conn.go index 0823e31..8a57641 100644 --- a/node/conn.go +++ b/node/conn.go @@ -35,6 +35,33 @@ func newConnWriter(conn *net.UDPConn, localIP byte, routing *routingTable) *conn return w } +/* + func (w *connWriter) SendRouting(remoteIP byte, data []byte) { + dstPeer := w.routing.Get(remoteIP) + if dstPeer == nil { + log.Printf("No peer: %d", remoteIP) + return + } + + var viaPeer *peer + + if dstPeer.Addr == zeroAddrPort { + viaPeer = w.routing.Mediator() + if viaPeer == nil { + log.Printf("No mediator: %d", remoteIP) + return + } + } + + w.sendRouting(dstPeer, viaPeer, data) + } +*/ + +func (w *connWriter) SendData(remoteIP byte, data []byte) { + // TODO +} + +// TODO: deprecated func (w *connWriter) WriteTo(remoteIP, stream byte, data []byte) { dstPeer := w.routing.Get(remoteIP) if dstPeer == nil { @@ -50,11 +77,11 @@ func (w *connWriter) WriteTo(remoteIP, stream byte, data []byte) { var viaPeer *peer if dstPeer.Mediated { viaPeer = w.routing.mediator.Load() - if viaPeer == nil || viaPeer.Addr == nil { + if viaPeer == nil || viaPeer.Addr == zeroAddrPort { log.Printf("Mediator not connected") return } - } else if dstPeer.Addr == nil { + } else if dstPeer.Addr == zeroAddrPort { log.Printf("Peer doesn't have address: %d", remoteIP) return } @@ -62,6 +89,7 @@ func (w *connWriter) WriteTo(remoteIP, stream byte, data []byte) { w.WriteToPeer(dstPeer, viaPeer, stream, data) } +// TODO: deprecated func (w *connWriter) WriteToPeer(dstPeer, viaPeer *peer, stream byte, data []byte) { w.lock.Lock() @@ -89,20 +117,21 @@ func (w *connWriter) WriteToPeer(dstPeer, viaPeer *peer, stream byte, data []byt addr = viaPeer.Addr } - if _, err := w.WriteToUDPAddrPort(buf, *addr); err != nil { + if _, err := w.WriteToUDPAddrPort(buf, addr); err != nil { log.Fatalf("Failed to write to UDP port: %v", err) } w.lock.Unlock() } +// TODO: deprecated func (w *connWriter) Forward(dstIP byte, packet []byte) { dstPeer := w.routing.Get(dstIP) - if dstPeer == nil || dstPeer.Addr == nil { + if dstPeer == nil || dstPeer.Addr == zeroAddrPort { log.Printf("No peer: %d", dstIP) return } - if _, err := w.WriteToUDPAddrPort(packet, *dstPeer.Addr); err != nil { + if _, err := w.WriteToUDPAddrPort(packet, dstPeer.Addr); err != nil { log.Fatalf("Failed to write to UDP port: %v", err) } } diff --git a/node/datacipher.go b/node/datacipher.go deleted file mode 100644 index b631d14..0000000 --- a/node/datacipher.go +++ /dev/null @@ -1,97 +0,0 @@ -package node - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "unsafe" -) - -// ---------------------------------------------------------------------------- - -const ( - dataStreamID = 1 - dataHeaderSize = 12 - dataCipherOverhead = 16 + 1 -) - -type dataHeader struct { - Counter uint64 // Init with fasttime.Now() << 30 to ensure monotonic. - SourceIP byte - DestIP byte -} - -func (h *dataHeader) Parse(b []byte) { - h.Counter = *(*uint64)(unsafe.Pointer(&b[0])) - h.SourceIP = b[8] - h.DestIP = b[9] -} - -func (h *dataHeader) Marshal(buf []byte) { - *(*uint64)(unsafe.Pointer(&buf[0])) = h.Counter - buf[8] = h.SourceIP - buf[9] = h.DestIP - buf[10] = 0 - buf[11] = 0 -} - -// ---------------------------------------------------------------------------- - -type dataCipher struct { - key []byte - aead cipher.AEAD -} - -func newDataCipher() *dataCipher { - key := make([]byte, 32) - if _, err := rand.Read(key); err != nil { - panic(err) - } - return newDataCipherFromKey(key) -} - -// key must be 32 bytes. -func newDataCipherFromKey(key []byte) *dataCipher { - block, err := aes.NewCipher(key) - if err != nil { - panic(err) - } - - aead, err := cipher.NewGCM(block) - if err != nil { - panic(err) - } - - return &dataCipher{key: key, aead: aead} -} - -func (sc *dataCipher) Key() []byte { - return sc.key -} - -func (sc *dataCipher) Encrypt(h *dataHeader, data, out []byte) []byte { - out = out[:dataHeaderSize+dataCipherOverhead+len(data)] - out[0] = dataStreamID - - h.Marshal(out[1:]) - - const s = dataHeaderSize - sc.aead.Seal(out[1+s:1+s], out[1:1+s], data, nil) - return out -} - -func (sc *dataCipher) Decrypt(encrypted, out []byte) (data []byte, h dataHeader, ok bool) { - const s = dataHeaderSize - if len(encrypted) < s+dataCipherOverhead { - ok = false - return - } - - h.Parse(encrypted[1 : 1+s]) - - var err error - - data, err = sc.aead.Open(out[:0], encrypted[1:1+s], encrypted[1+s:], nil) - ok = err == nil - return -} diff --git a/node/header.go b/node/header.go index ed3671a..a409576 100644 --- a/node/header.go +++ b/node/header.go @@ -2,6 +2,41 @@ package node import "unsafe" +// ---------------------------------------------------------------------------- + +const ( + routingStreamID = 2 + routingHeaderSize = 24 + routingCipherOverhead = 16 + + dataStreamID = 1 + dataHeaderSize = 12 + dataCipherOverhead = 16 +) + +// TODO: Rename +type xHeader struct { + Counter uint64 // Init with fasttime.Now() << 30 to ensure monotonic. + SourceIP byte + DestIP byte +} + +func (h *xHeader) Parse(b []byte) { + h.Counter = *(*uint64)(unsafe.Pointer(&b[1])) + h.SourceIP = b[9] + h.DestIP = b[10] +} + +func (h *xHeader) Marshal(streamID byte, buf []byte) { + buf[0] = streamID + *(*uint64)(unsafe.Pointer(&buf[1])) = h.Counter + buf[9] = h.SourceIP + buf[10] = h.DestIP + buf[11] = 0 +} + +// ---------------------------------------------------------------------------- +// TODO: Remove this code. const ( headerSize = 24 streamData = 1 diff --git a/node/packets.go b/node/packets.go new file mode 100644 index 0000000..75f4e6e --- /dev/null +++ b/node/packets.go @@ -0,0 +1,89 @@ +package node + +import ( + "errors" + "net/netip" + "time" + "unsafe" +) + +var errMalformedPacket = errors.New("malformed packet") + +const ( + packetTypePing = iota + 1 + packetTypePong +) + +// ---------------------------------------------------------------------------- + +type packetWrapper struct { + SrcIP byte + RemoteAddr netip.AddrPort + Packet any +} + +// ---------------------------------------------------------------------------- + +// A pingPacket is sent from a node acting as a client, to a node acting +// as a server. It always contains the shared key the client is expecting +// to use for data encryption with the server. +type pingPacket struct { + SentAt int64 // UnixMilli. + SharedKey [32]byte +} + +func newPingPacket(sharedKey []byte) (pp pingPacket) { + pp.SentAt = time.Now().UnixMilli() + copy(pp.SharedKey[:], sharedKey) + return +} + +func (p pingPacket) Marshal(buf []byte) []byte { + buf = buf[:41] + buf[0] = packetTypePing + *(*uint64)(unsafe.Pointer(&buf[1])) = uint64(p.SentAt) + copy(buf[9:41], p.SharedKey[:]) + return buf +} + +func (p *pingPacket) Parse(buf []byte) error { + if len(buf) != 41 { + return errMalformedPacket + } + p.SentAt = *(*int64)(unsafe.Pointer(&buf[1])) + copy(p.SharedKey[:], buf[9:41]) + return nil +} + +// ---------------------------------------------------------------------------- + +// A pongPacket is sent by a node in a server role in response to a pingPacket. +type pongPacket struct { + SentAt int64 // UnixMilli. + RecvdAt int64 // UnixMilli. +} + +func newPongPacket(sentAt int64) (pp pongPacket) { + pp.SentAt = sentAt + pp.RecvdAt = time.Now().UnixMilli() + return +} + +func (p pongPacket) Marshal(buf []byte) []byte { + buf = buf[:17] + buf[0] = packetTypePong + *(*uint64)(unsafe.Pointer(&buf[1])) = uint64(p.SentAt) + *(*uint64)(unsafe.Pointer(&buf[9])) = uint64(p.RecvdAt) + + return buf +} + +func (p *pongPacket) Parse(buf []byte) error { + if len(buf) != 17 { + return errMalformedPacket + } + p.SentAt = *(*int64)(unsafe.Pointer(&buf[1])) + p.RecvdAt = *(*int64)(unsafe.Pointer(&buf[9])) + + return nil +} diff --git a/node/packets_test.go b/node/packets_test.go new file mode 100644 index 0000000..bd89215 --- /dev/null +++ b/node/packets_test.go @@ -0,0 +1,42 @@ +package node + +import ( + "crypto/rand" + "reflect" + "testing" +) + +func TestPacketPing(t *testing.T) { + sharedKey := make([]byte, 32) + rand.Read(sharedKey) + + buf := make([]byte, bufferSize) + + p := newPingPacket(sharedKey) + out := p.Marshal(buf) + + p2 := pingPacket{} + if err := p2.Parse(out); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(p, p2) { + t.Fatal(p, p2) + } +} + +func TestPacketPong(t *testing.T) { + buf := make([]byte, bufferSize) + + p := newPongPacket(123566) + out := p.Marshal(buf) + + p2 := pongPacket{} + if err := p2.Parse(out); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(p, p2) { + t.Fatal(p, p2) + } +} diff --git a/node/peersupervisor.go b/node/peersupervisor.go index 14c9315..bdcf03f 100644 --- a/node/peersupervisor.go +++ b/node/peersupervisor.go @@ -36,7 +36,7 @@ type peerSupervisor struct { // Peer-related items. version int64 // Ony accessed in HandlePeerUpdate. peer *m.Peer - remoteAddrPort *netip.AddrPort + remoteAddrPort netip.AddrPort mediated bool sharedKey []byte @@ -123,9 +123,9 @@ func (s *peerSupervisor) stateInit() stateFunc { addr, ok := netip.AddrFromSlice(s.peer.PublicIP) if ok { addrPort := netip.AddrPortFrom(addr, s.peer.Port) - s.remoteAddrPort = &addrPort + s.remoteAddrPort = addrPort } else { - s.remoteAddrPort = nil + s.remoteAddrPort = zeroAddrPort } s.sharedKey = computeSharedKey(s.peer.EncPubKey, s.privKey) @@ -153,7 +153,7 @@ func (s *peerSupervisor) stateSelectRole() stateFunc { s.logf("STATE: SelectRole") s.updateRoutingTable(false) - if s.remoteAddrPort != nil { + if s.remoteAddrPort != zeroAddrPort { s.mediated = false // If both remote and local are public, one side acts as client, and one @@ -186,7 +186,7 @@ func (s *peerSupervisor) stateAccept() stateFunc { switch pkt.Type { case packetTypePing: - s.remoteAddrPort = &pkt.Addr + s.remoteAddrPort = pkt.Addr s.updateRoutingTable(true) s.sendPong(pkt.TraceID) return s.stateConnected @@ -256,8 +256,8 @@ func (s *peerSupervisor) stateConnected() stateFunc { // Server should always follow remote port. if s.localPublic { - if pkt.Addr != *s.remoteAddrPort { - s.remoteAddrPort = &pkt.Addr + if pkt.Addr != s.remoteAddrPort { + s.remoteAddrPort = pkt.Addr s.updateRoutingTable(true) } } diff --git a/node/router.go b/node/router.go index 67c0756..c99f763 100644 --- a/node/router.go +++ b/node/router.go @@ -12,12 +12,18 @@ import ( "vppn/m" ) +var zeroAddrPort = netip.AddrPort{} + type peer struct { - Up bool // No data will be sent to peers that are down. - Mediator bool + IP byte // The VPN IP. + Up bool // No data will be sent to peers that are down. + Addr netip.AddrPort // If we have direct connection, otherwise use mediator. + Mediator bool // True if the peer will mediate. + RoutingCipher routingCipher + DataCipher dataCipher + + // TODO: Deprecated below. Mediated bool - IP byte - Addr *netip.AddrPort // If we have direct connection, otherwise use mediator. SharedKey []byte } @@ -48,6 +54,10 @@ func (r *routingTable) Set(ip byte, p *peer) { r.table[ip].Store(p) } +func (r *routingTable) Mediator() *peer { + return r.mediator.Load() +} + // ---------------------------------------------------------------------------- type router struct { diff --git a/node/routingpacket.go b/node/routingpacket.go index 64f0374..1b5aed1 100644 --- a/node/routingpacket.go +++ b/node/routingpacket.go @@ -1,18 +1,9 @@ package node import ( - "errors" "unsafe" ) -var errMalformedPacket = errors.New("malformed packet") - -const ( - // Used to maintain connection. - packetTypePing = iota + 1 - packetTypePong -) - type routingPacket struct { Type byte // One of the packetType* constants. TraceID uint64 // For matching requests and responses.