From bfdcf0762f0bb63419a856039ac1673c78c9e03c Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 1 Feb 2024 22:23:16 +0000 Subject: [PATCH] Link cost-aware routing --- cmd/yggdrasilctl/main.go | 3 ++- src/admin/getpeers.go | 2 ++ src/core/api.go | 2 ++ src/core/link.go | 25 +++++++++++++++++++++++-- src/core/version.go | 19 ++++++++++++++++--- src/core/version_test.go | 3 +++ src/multicast/multicast.go | 4 ++++ src/multicast/options.go | 1 + 8 files changed, 53 insertions(+), 6 deletions(-) diff --git a/cmd/yggdrasilctl/main.go b/cmd/yggdrasilctl/main.go index 2a1d70b1..8a30f438 100644 --- a/cmd/yggdrasilctl/main.go +++ b/cmd/yggdrasilctl/main.go @@ -174,7 +174,7 @@ func run() int { if err := json.Unmarshal(recv.Response, &resp); err != nil { panic(err) } - table.SetHeader([]string{"URI", "State", "Dir", "IP Address", "Uptime", "RTT", "RX", "TX", "Pr", "Last Error"}) + table.SetHeader([]string{"URI", "State", "Dir", "IP Address", "Uptime", "RTT", "RX", "TX", "Pr", "Cost", "Last Error"}) for _, peer := range resp.Peers { state, lasterr, dir, rtt := "Up", "-", "Out", "-" if !peer.Up { @@ -200,6 +200,7 @@ func run() int { peer.RXBytes.String(), peer.TXBytes.String(), fmt.Sprintf("%d", peer.Priority), + fmt.Sprintf("%d", peer.Cost), lasterr, }) } diff --git a/src/admin/getpeers.go b/src/admin/getpeers.go index e44428c3..591bf3ea 100644 --- a/src/admin/getpeers.go +++ b/src/admin/getpeers.go @@ -24,6 +24,7 @@ type PeerEntry struct { PublicKey string `json:"key"` Port uint64 `json:"port"` Priority uint64 `json:"priority"` + Cost uint64 `json:"cost"` RXBytes DataUnit `json:"bytes_recvd,omitempty"` TXBytes DataUnit `json:"bytes_sent,omitempty"` Uptime float64 `json:"uptime,omitempty"` @@ -41,6 +42,7 @@ func (a *AdminSocket) getPeersHandler(_ *GetPeersRequest, res *GetPeersResponse) Up: p.Up, Inbound: p.Inbound, Priority: uint64(p.Priority), // can't be uint8 thanks to gobind + Cost: uint64(p.Cost), // can't be uint8 thanks to gobind URI: p.URI, RXBytes: DataUnit(p.RXBytes), TXBytes: DataUnit(p.TXBytes), diff --git a/src/core/api.go b/src/core/api.go index 875d7bf2..82721fe6 100644 --- a/src/core/api.go +++ b/src/core/api.go @@ -30,6 +30,7 @@ type PeerInfo struct { Coords []uint64 Port uint64 Priority uint8 + Cost uint8 RXBytes uint64 TXBytes uint64 Uptime time.Duration @@ -94,6 +95,7 @@ func (c *Core) GetPeers() []PeerInfo { peerinfo.Port = p.Port peerinfo.Priority = p.Priority peerinfo.Latency = p.Latency + peerinfo.Cost = p.Cost } peers = append(peers, peerinfo) } diff --git a/src/core/link.go b/src/core/link.go index f45c2cee..2435a638 100644 --- a/src/core/link.go +++ b/src/core/link.go @@ -70,6 +70,7 @@ type link struct { type linkOptions struct { pinnedEd25519Keys map[keyArray]struct{} priority uint8 + cost uint8 tlsSNI string password []byte maxBackoff time.Duration @@ -139,6 +140,7 @@ func (e linkError) Error() string { return string(e) } const ErrLinkAlreadyConfigured = linkError("peer is already configured") const ErrLinkNotConfigured = linkError("peer is not configured") const ErrLinkPriorityInvalid = linkError("priority value is invalid") +const ErrLinkCostInvalid = linkError("cost value is invalid") const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid") const ErrLinkPasswordInvalid = linkError("password is invalid") const ErrLinkUnrecognisedSchema = linkError("link schema unknown") @@ -181,6 +183,14 @@ func (l *links) add(u *url.URL, sintf string, linkType linkType) error { } options.priority = uint8(pi) } + if p := u.Query().Get("cost"); p != "" { + c, err := strconv.ParseUint(p, 10, 8) + if err != nil { + retErr = ErrLinkCostInvalid + return + } + options.cost = uint8(c) + } if p := u.Query().Get("password"); p != "" { if len(p) > blake2b.Size { retErr = ErrLinkPasswordInvalid @@ -448,6 +458,13 @@ func (l *links) listen(u *url.URL, sintf string) (*Listener, error) { } options.priority = uint8(pi) } + if p := u.Query().Get("cost"); p != "" { + c, err := strconv.ParseUint(p, 10, 8) + if err != nil { + return nil, ErrLinkCostInvalid + } + options.cost = uint8(c) + } if p := u.Query().Get("password"); p != "" { if len(p) > blake2b.Size { return nil, ErrLinkPasswordInvalid @@ -567,6 +584,7 @@ func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn, s meta := version_getBaseMetadata() meta.publicKey = l.core.public meta.priority = options.priority + meta.cost = options.cost metaBytes, err := meta.encode(l.core.secret, options.password) if err != nil { return fmt.Errorf("failed to generate handshake: %w", err) @@ -628,17 +646,20 @@ func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn, s remoteAddr := net.IP(address.AddrForKey(meta.publicKey)[:]).String() remoteStr := fmt.Sprintf("%s@%s", remoteAddr, conn.RemoteAddr()) localStr := conn.LocalAddr() - priority := options.priority + cost, priority := options.cost, options.priority if meta.priority > priority { priority = meta.priority } + if meta.cost > cost { + cost = meta.cost + } l.core.log.Infof("Connected %s: %s, source %s", dir, remoteStr, localStr) if success != nil { success() } - err = l.core.HandleConn(meta.publicKey, conn, priority) + err = l.core.HandleConn(meta.publicKey, conn, cost, priority) switch err { case io.EOF, net.ErrClosed, nil: l.core.log.Infof("Disconnected %s: %s, source %s", diff --git a/src/core/version.go b/src/core/version.go index 28b16430..f38b88f8 100644 --- a/src/core/version.go +++ b/src/core/version.go @@ -22,6 +22,7 @@ type version_metadata struct { minorVer uint16 publicKey ed25519.PublicKey priority uint8 + cost uint8 } const ( @@ -36,6 +37,7 @@ const ( metaVersionMinor // uint16 metaPublicKey // [32]byte metaPriority // uint8 + metaCost // uint8 ) // Gets a base metadata with no keys set, but with the correct version numbers. @@ -64,9 +66,17 @@ func (m *version_metadata) encode(privateKey ed25519.PrivateKey, password []byte bs = binary.BigEndian.AppendUint16(bs, ed25519.PublicKeySize) bs = append(bs, m.publicKey[:]...) - bs = binary.BigEndian.AppendUint16(bs, metaPriority) - bs = binary.BigEndian.AppendUint16(bs, 1) - bs = append(bs, m.priority) + if m.priority > 0 { + bs = binary.BigEndian.AppendUint16(bs, metaPriority) + bs = binary.BigEndian.AppendUint16(bs, 1) + bs = append(bs, m.priority) + } + + if m.cost > 0 { + bs = binary.BigEndian.AppendUint16(bs, metaCost) + bs = binary.BigEndian.AppendUint16(bs, 1) + bs = append(bs, m.cost) + } hasher, err := blake2b.New512(password) if err != nil { @@ -126,6 +136,9 @@ func (m *version_metadata) decode(r io.Reader, password []byte) error { case metaPriority: m.priority = bs[0] + + case metaCost: + m.cost = bs[0] } bs = bs[oplen:] } diff --git a/src/core/version_test.go b/src/core/version_test.go index 512c6e59..c9b50ed5 100644 --- a/src/core/version_test.go +++ b/src/core/version_test.go @@ -52,6 +52,9 @@ func TestVersionRoundtrip(t *testing.T) { {majorVer: 258, minorVer: 259}, {majorVer: 3, minorVer: 5, priority: 6}, {majorVer: 260, minorVer: 261, priority: 7}, + {majorVer: 258, minorVer: 259, cost: 5}, + {majorVer: 3, minorVer: 5, priority: 6, cost: 12}, + {majorVer: 260, minorVer: 261, priority: 7, cost: 1}, } { // Generate a random public key for each time, since it is // a required field. diff --git a/src/multicast/multicast.go b/src/multicast/multicast.go index bf10370f..8e6a36c0 100644 --- a/src/multicast/multicast.go +++ b/src/multicast/multicast.go @@ -45,6 +45,7 @@ type interfaceInfo struct { listen bool port uint16 priority uint8 + cost uint8 password []byte hash []byte } @@ -214,6 +215,7 @@ func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo { listen: ifcfg.Listen, port: ifcfg.Port, priority: ifcfg.Priority, + cost: ifcfg.Cost, password: []byte(ifcfg.Password), hash: hasher.Sum(nil), } @@ -314,6 +316,7 @@ func (m *Multicast) _announce() { // No listener was found - let's create one v := &url.Values{} v.Add("priority", fmt.Sprintf("%d", info.priority)) + v.Add("cost", fmt.Sprintf("%d", info.cost)) v.Add("password", string(info.password)) u := &url.URL{ Scheme: "tls", @@ -428,6 +431,7 @@ func (m *Multicast) listen() { v := &url.Values{} v.Add("key", hex.EncodeToString(adv.PublicKey)) v.Add("priority", fmt.Sprintf("%d", info.priority)) + v.Add("cost", fmt.Sprintf("%d", info.cost)) v.Add("password", string(info.password)) u := &url.URL{ Scheme: "tls", diff --git a/src/multicast/options.go b/src/multicast/options.go index bd9fea5a..2bfbb0af 100644 --- a/src/multicast/options.go +++ b/src/multicast/options.go @@ -21,6 +21,7 @@ type MulticastInterface struct { Listen bool Port uint16 Priority uint8 + Cost uint8 Password string }