fix(config): error on ambiguous peer ID prefix (#113)

FindPeerByIDPrefix previously returned the first peer whose ID started
with the given prefix, silently ignoring further matches. With Ed25519
peer IDs all sharing the leading "12D3KooW" (8 chars), short prefixes
can match multiple peers; the chosen peer then depends on slice order,
which is effectively undefined from the user's perspective.

Change FindPeerByIDPrefix and its wrapper FindPeerByCLIRef to return
(*Peer, error):

  - 0 matches -> (nil, nil)
  - 1 match   -> (&peer, nil)
  - 2+ match  -> (nil, error) describing the ambiguity

Call sites in config.Read and rpc/server.go updated accordingly:
ambiguity surfaces as a config-load / CLI error instead of a silent
wrong-peer resolution.

Not a wire-protocol or config-format change. Only behavior visible at
CLI invocation and config load. Users relying on short prefixes that
happened to match multiple peers will now see a clear error and must
lengthen the prefix.
This commit is contained in:
Roland Urbano 2026-05-23 22:26:05 +02:00 committed by GitHub
parent 71eeca5801
commit 3f573ef293
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 36 additions and 15 deletions

View File

@ -166,15 +166,21 @@ func Read(path string) (*Config, error) {
whitelist := make(map[peer.ID]struct{})
blacklist := make(map[peer.ID]struct{})
for _, p := range service.Acl.Whitelist {
cfgPeer, found := FindPeerByCLIRef(result.Peers, p)
if !found {
cfgPeer, err := FindPeerByCLIRef(result.Peers, p)
if err != nil {
return nil, err
}
if cfgPeer == nil {
return nil, errors.New("unknown peer: " + p)
}
whitelist[cfgPeer.ID] = struct{}{}
}
for _, peerStr := range service.Acl.Blacklist {
cfgPeer, found := FindPeerByCLIRef(result.Peers, peerStr)
if !found {
cfgPeer, err := FindPeerByCLIRef(result.Peers, peerStr)
if err != nil {
return nil, err
}
if cfgPeer == nil {
return nil, errors.New("unknown peer: " + peerStr)
}
blacklist[cfgPeer.ID] = struct{}{}
@ -212,22 +218,34 @@ func FindPeerByName(peers []Peer, needle string) (*Peer, bool) {
return nil, false
}
func FindPeerByIDPrefix(peers []Peer, needle string) (*Peer, bool) {
for _, p := range peers {
if strings.HasPrefix(p.ID.String(), needle) {
return &p, true
// FindPeerByIDPrefix returns the unique peer whose ID starts with needle.
// Returns (nil, nil) if no peer matches.
// Returns (nil, error) if more than one peer matches (ambiguous prefix).
func FindPeerByIDPrefix(peers []Peer, needle string) (*Peer, error) {
var match *Peer
count := 0
for i := range peers {
if strings.HasPrefix(peers[i].ID.String(), needle) {
match = &peers[i]
count++
}
}
return nil, false
if count > 1 {
return nil, fmt.Errorf("ambiguous peer prefix %q matches %d peers", needle, count)
}
return match, nil
}
func FindPeerByCLIRef(peers []Peer, needle string) (*Peer, bool) {
// FindPeerByCLIRef resolves a CLI peer reference: "@name" for name lookup,
// otherwise a peer ID prefix. Returns (nil, nil) on no match,
// (nil, error) only on ambiguous ID prefix.
func FindPeerByCLIRef(peers []Peer, needle string) (*Peer, error) {
if strings.HasPrefix(needle, "@") {
name := strings.TrimPrefix(needle, "@")
return FindPeerByName(peers, name)
} else {
return FindPeerByIDPrefix(peers, needle)
p, _ := FindPeerByName(peers, name)
return p, nil
}
return FindPeerByIDPrefix(peers, needle)
}
func (cfg Config) FindRoute(needle net.IPNet) (*RouteTableEntry, bool) {

View File

@ -120,8 +120,11 @@ func (hsr *HyprspaceRPC) Route(args *RouteArgs, reply *RouteReply) error {
if err != nil {
return err
}
target, found := config.FindPeerByCLIRef(hsr.config.Peers, args.Args[1])
if !found {
target, err := config.FindPeerByCLIRef(hsr.config.Peers, args.Args[1])
if err != nil {
return err
}
if target == nil {
return errors.New("no such peer")
}
err = hsr.tunDev.Apply(tun.Route(*network))