Skip to content

Commit effd506

Browse files
committed
Activate network primitives, SYN trust gate, hostname privacy
- Remove WIP guards from create_network, join_network, leave_network handlers, enabling all network operations - Add SYN trust gate: private nodes reject connections from untrusted peers that don't share a non-backbone network (sends RST, emits syn.rejected webhook) - Add hostname privacy: resolve_hostname checks trust pairs and shared networks before revealing private node info - Add check_trust registry message for unified trust+network queries - Gate broadcast: block on backbone (network 0), require membership - Add ResolveHostnameAs client method for trust-aware hostname resolution - Unskip 15 tests (network, admin token, broadcast, handshake) - Add 8 new tests (4 SYN trust gate, 4 hostname privacy)
1 parent 44768fc commit effd506

11 files changed

Lines changed: 552 additions & 28 deletions

pkg/daemon/daemon.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,14 @@ func (d *Daemon) Identity() *crypto.Identity { return d.identity }
547547
// TaskQueue returns the daemon's task queue.
548548
func (d *Daemon) TaskQueue() *TaskQueue { return d.taskQueue }
549549

550+
// AddTunnelPeer registers a peer's address in the tunnel manager (for testing/manual setup).
551+
func (d *Daemon) AddTunnelPeer(nodeID uint32, addr *net.UDPAddr) {
552+
d.tunnels.AddPeer(nodeID, addr)
553+
}
554+
555+
// TunnelAddr returns the local UDP address of the tunnel listener.
556+
func (d *Daemon) TunnelAddr() net.Addr { return d.tunnels.LocalAddr() }
557+
550558
func (d *Daemon) Addr() protocol.Addr {
551559
d.addrMu.RLock()
552560
defer d.addrMu.RUnlock()
@@ -728,6 +736,26 @@ func (d *Daemon) handleStreamPacket(pkt *protocol.Packet) {
728736
return
729737
}
730738

739+
// Trust gate: private nodes only accept SYN from trusted or same-network peers
740+
if !d.config.Public {
741+
srcNode := pkt.Src.Node
742+
trusted := d.handshakes.IsTrusted(srcNode)
743+
if !trusted && d.regConn != nil {
744+
// Fall back to registry trust check (covers admin-set trust pairs + shared networks)
745+
trusted, _ = d.regConn.CheckTrust(d.NodeID(), srcNode)
746+
}
747+
if !trusted {
748+
slog.Warn("SYN rejected: untrusted source", "src_node", srcNode, "src_addr", pkt.Src, "dst_port", pkt.DstPort)
749+
d.webhook.Emit("syn.rejected", map[string]interface{}{
750+
"src_node_id": srcNode,
751+
"src_addr": pkt.Src.String(),
752+
"dst_port": pkt.DstPort,
753+
})
754+
d.sendRST(pkt)
755+
return
756+
}
757+
}
758+
731759
conn := d.ports.NewConnection(pkt.DstPort, pkt.Src, pkt.SrcPort)
732760
conn.Mu.Lock()
733761
conn.LocalAddr = d.Addr()
@@ -1520,7 +1548,7 @@ func (d *Daemon) SendDatagram(dstAddr protocol.Addr, dstPort uint16, data []byte
15201548
srcPort := d.ports.AllocEphemeralPort()
15211549

15221550
if dstAddr.IsBroadcast() {
1523-
return fmt.Errorf("broadcast is not available — custom networks are WIP")
1551+
return d.broadcastDatagram(dstAddr.Network, srcPort, dstPort, data)
15241552
}
15251553

15261554
if err := d.ensureTunnel(dstAddr.Node); err != nil {
@@ -1541,7 +1569,12 @@ func (d *Daemon) SendDatagram(dstAddr protocol.Addr, dstPort uint16, data []byte
15411569
}
15421570

15431571
// broadcastDatagram sends a datagram to all members of a network.
1572+
// Only network members are allowed to broadcast. Backbone (network 0) is blocked.
15441573
func (d *Daemon) broadcastDatagram(netID uint16, srcPort, dstPort uint16, data []byte) error {
1574+
if netID == 0 {
1575+
return fmt.Errorf("broadcast on backbone network is not permitted")
1576+
}
1577+
15451578
resp, err := d.regConn.ListNodes(netID)
15461579
if err != nil {
15471580
return fmt.Errorf("list nodes for broadcast: %w", err)
@@ -1552,6 +1585,22 @@ func (d *Daemon) broadcastDatagram(netID uint16, srcPort, dstPort uint16, data [
15521585
return nil // no nodes
15531586
}
15541587

1588+
// Verify sender is a member of the network
1589+
isMember := false
1590+
for _, n := range nodesRaw {
1591+
nodeMap, ok := n.(map[string]interface{})
1592+
if !ok {
1593+
continue
1594+
}
1595+
if nid, ok := nodeMap["node_id"].(float64); ok && uint32(nid) == d.NodeID() {
1596+
isMember = true
1597+
break
1598+
}
1599+
}
1600+
if !isMember {
1601+
return fmt.Errorf("broadcast denied: node %d is not a member of network %d", d.NodeID(), netID)
1602+
}
1603+
15551604
for _, n := range nodesRaw {
15561605
nodeMap, ok := n.(map[string]interface{})
15571606
if !ok {

pkg/registry/client.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,30 @@ func (c *Client) ResolveHostname(hostname string) (map[string]interface{}, error
450450
})
451451
}
452452

453+
// ResolveHostnameAs resolves a hostname with a requester_id for privacy checks.
454+
// Private nodes require the requester to have a trust pair or shared network.
455+
func (c *Client) ResolveHostnameAs(requesterID uint32, hostname string) (map[string]interface{}, error) {
456+
return c.Send(map[string]interface{}{
457+
"type": "resolve_hostname",
458+
"hostname": hostname,
459+
"requester_id": requesterID,
460+
})
461+
}
462+
463+
// CheckTrust checks if a trust pair or shared network exists between two nodes.
464+
func (c *Client) CheckTrust(nodeA, nodeB uint32) (bool, error) {
465+
resp, err := c.Send(map[string]interface{}{
466+
"type": "check_trust",
467+
"node_id": nodeA,
468+
"peer_id": nodeB,
469+
})
470+
if err != nil {
471+
return false, err
472+
}
473+
trusted, _ := resp["trusted"].(bool)
474+
return trusted, nil
475+
}
476+
453477
// UpdatePoloScore adjusts the polo score of a node by the given delta.
454478
// Delta can be positive (increase polo score) or negative (decrease polo score).
455479
func (c *Client) UpdatePoloScore(nodeID uint32, delta int) (map[string]interface{}, error) {

pkg/registry/server.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -696,11 +696,11 @@ func (s *Server) handleMessage(msg map[string]interface{}, remoteAddr string) (r
696696
}
697697
return s.handleRegister(msg, remoteAddr)
698698
case "create_network":
699-
return nil, fmt.Errorf("custom networks are WIP — only backbone (network 0) is available")
699+
return s.handleCreateNetwork(msg)
700700
case "join_network":
701-
return nil, fmt.Errorf("custom networks are WIP — only backbone (network 0) is available")
701+
return s.handleJoinNetwork(msg)
702702
case "leave_network":
703-
return nil, fmt.Errorf("custom networks are WIP — only backbone (network 0) is available")
703+
return s.handleLeaveNetwork(msg)
704704
case "lookup":
705705
return s.handleLookup(msg)
706706
case "resolve":
@@ -725,6 +725,8 @@ func (s *Server) handleMessage(msg map[string]interface{}, remoteAddr string) (r
725725
return s.handleReportTrust(msg)
726726
case "revoke_trust":
727727
return s.handleRevokeTrust(msg)
728+
case "check_trust":
729+
return s.handleCheckTrust(msg)
728730
case "request_handshake":
729731
return s.handleRequestHandshake(msg)
730732
case "poll_handshakes":
@@ -1488,6 +1490,44 @@ func (s *Server) handleRevokeTrust(msg map[string]interface{}) (map[string]inter
14881490
}, nil
14891491
}
14901492

1493+
// handleCheckTrust checks if a trust pair exists between two nodes OR if they share a non-backbone network.
1494+
func (s *Server) handleCheckTrust(msg map[string]interface{}) (map[string]interface{}, error) {
1495+
nodeA := jsonUint32(msg, "node_id")
1496+
nodeB := jsonUint32(msg, "peer_id")
1497+
1498+
s.mu.RLock()
1499+
defer s.mu.RUnlock()
1500+
1501+
trusted := s.trustPairs[trustPairKey(nodeA, nodeB)]
1502+
1503+
// Also check shared non-backbone network
1504+
if !trusted {
1505+
nA, okA := s.nodes[nodeA]
1506+
nB, okB := s.nodes[nodeB]
1507+
if okA && okB {
1508+
for _, aNet := range nA.Networks {
1509+
if aNet == 0 {
1510+
continue
1511+
}
1512+
for _, bNet := range nB.Networks {
1513+
if aNet == bNet {
1514+
trusted = true
1515+
break
1516+
}
1517+
}
1518+
if trusted {
1519+
break
1520+
}
1521+
}
1522+
}
1523+
}
1524+
1525+
return map[string]interface{}{
1526+
"type": "check_trust_ok",
1527+
"trusted": trusted,
1528+
}, nil
1529+
}
1530+
14911531
func (s *Server) handleSetVisibility(msg map[string]interface{}) (map[string]interface{}, error) {
14921532
nodeID := jsonUint32(msg, "node_id")
14931533
public, _ := msg["public"].(bool)
@@ -1848,6 +1888,46 @@ func (s *Server) handleResolveHostname(msg map[string]interface{}) (map[string]i
18481888
return nil, fmt.Errorf("hostname %q maps to missing node %d", hostname, nodeID)
18491889
}
18501890

1891+
// Privacy check: private nodes require trust or shared network
1892+
if !node.Public {
1893+
requesterID := jsonUint32(msg, "requester_id")
1894+
allowed := false
1895+
1896+
// Self-resolve always allowed
1897+
if requesterID == nodeID {
1898+
allowed = true
1899+
}
1900+
1901+
// Check trust pair
1902+
if !allowed && s.trustPairs[trustPairKey(requesterID, nodeID)] {
1903+
allowed = true
1904+
}
1905+
1906+
// Check shared non-backbone network
1907+
if !allowed {
1908+
if requester, rOk := s.nodes[requesterID]; rOk {
1909+
for _, rNet := range requester.Networks {
1910+
if rNet == 0 {
1911+
continue
1912+
}
1913+
for _, tNet := range node.Networks {
1914+
if rNet == tNet {
1915+
allowed = true
1916+
break
1917+
}
1918+
}
1919+
if allowed {
1920+
break
1921+
}
1922+
}
1923+
}
1924+
}
1925+
1926+
if !allowed {
1927+
return nil, fmt.Errorf("resolve denied: hostname %q belongs to a private node", hostname)
1928+
}
1929+
}
1930+
18511931
return map[string]interface{}{
18521932
"type": "resolve_hostname_ok",
18531933
"node_id": node.ID,

tests/admin_token_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
// TestAdminTokenRequired verifies that with an admin token configured,
1212
// create_network requires the correct token.
1313
func TestAdminTokenRequired(t *testing.T) {
14-
t.Skip("custom networks are WIP")
1514
t.Parallel()
1615
env := NewTestEnv(t)
1716

@@ -54,7 +53,6 @@ func TestAdminTokenRequired(t *testing.T) {
5453
// TestAdminTokenJoinLeaveGated verifies that join_network and leave_network
5554
// also require admin token.
5655
func TestAdminTokenJoinLeaveGated(t *testing.T) {
57-
t.Skip("custom networks are WIP")
5856
t.Parallel()
5957
env := NewTestEnv(t)
6058

tests/broadcast_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
)
1111

1212
func TestBroadcast(t *testing.T) {
13-
t.Skip("custom networks are WIP")
1413
t.Parallel()
1514
env := NewTestEnv(t)
1615

tests/fuzz_registry_server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ func TestRegistryClientSetHostname(t *testing.T) {
640640
t.Fatalf("expected set_hostname_ok, got %v", hnResp["type"])
641641
}
642642

643-
resolveResp, err := c.ResolveHostname("myagent")
643+
resolveResp, err := c.ResolveHostnameAs(nodeID, "myagent")
644644
if err != nil {
645645
t.Fatalf("ResolveHostname: %v", err)
646646
}

tests/handshake_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,6 @@ func TestHandshakePendingApproveReject(t *testing.T) {
259259
}
260260

261261
func TestHandshakeNetworkTrust(t *testing.T) {
262-
t.Skip("custom networks are WIP")
263262
t.Parallel()
264263
// Two nodes on the same non-backbone network should auto-approve handshakes
265264

0 commit comments

Comments
 (0)