Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ repos:
language: system
files: \.go$
pass_filenames: false
stages: [commit]
stages: [pre-commit]
2 changes: 1 addition & 1 deletion cmd/pilotctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
"github.com/TeoSlayer/pilotprotocol/pkg/logging"
"github.com/TeoSlayer/pilotprotocol/pkg/protocol"
"github.com/TeoSlayer/pilotprotocol/pkg/registry"
"github.com/TeoSlayer/pilotprotocol/pkg/tasksubmit"
"github.com/TeoSlayer/pilotprotocol/pkg/tasksubmit"
)

// Global flags
Expand Down
4 changes: 2 additions & 2 deletions pkg/beacon/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ func TestCrossBeaconRelay(t *testing.T) {
payload := []byte("hello from node 10")
relayMsg := make([]byte, 1+4+4+len(payload))
relayMsg[0] = protocol.BeaconMsgRelay
binary.BigEndian.PutUint32(relayMsg[1:5], 10) // sender
binary.BigEndian.PutUint32(relayMsg[5:9], 20) // dest
binary.BigEndian.PutUint32(relayMsg[1:5], 10) // sender
binary.BigEndian.PutUint32(relayMsg[5:9], 20) // dest
copy(relayMsg[9:], payload)

if _, err := conn1.Write(relayMsg); err != nil {
Expand Down
9 changes: 4 additions & 5 deletions pkg/daemon/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/TeoSlayer/pilotprotocol/pkg/protocol"
)


// Handshake message types
const (
HandshakeRequest = "handshake_request"
Expand Down Expand Up @@ -55,11 +54,11 @@ type PendingHandshake struct {

// Handshake timing constants
const (
handshakeMaxAge = 5 * time.Minute // replay protection: max message age
handshakeMaxFuture = 30 * time.Second // replay protection: max clock skew
handshakeMaxAge = 5 * time.Minute // replay protection: max message age
handshakeMaxFuture = 30 * time.Second // replay protection: max clock skew
handshakeReapInterval = 5 * time.Minute // how often to reap stale replay entries
handshakeRecvTimeout = 10 * time.Second // time to wait for handshake message
handshakeCloseDelay = 500 * time.Millisecond // delay before closing after send to let data flush
handshakeRecvTimeout = 10 * time.Second // time to wait for handshake message
handshakeCloseDelay = 500 * time.Millisecond // delay before closing after send to let data flush
)

// HandshakeManager handles the trust handshake protocol on port 444.
Expand Down
1 change: 0 additions & 1 deletion pkg/daemon/ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,4 +779,3 @@ func (s *IPCServer) DeliverDatagram(srcAddr protocol.Addr, srcPort uint16, dstPo
}
}
}

2 changes: 1 addition & 1 deletion pkg/daemon/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/TeoSlayer/pilotprotocol/pkg/dataexchange"
"github.com/TeoSlayer/pilotprotocol/pkg/eventstream"
"github.com/TeoSlayer/pilotprotocol/pkg/protocol"
"github.com/TeoSlayer/pilotprotocol/pkg/registry"
"github.com/TeoSlayer/pilotprotocol/pkg/registry"
"github.com/TeoSlayer/pilotprotocol/pkg/tasksubmit"
)

Expand Down
4 changes: 2 additions & 2 deletions pkg/daemon/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ type TunnelManager struct {
pending map[uint32][][]byte // node_id → queued frames

// NAT traversal: beacon-coordinated hole-punching and relay
beaconAddr *net.UDPAddr // beacon address for punch/relay
relayPeers map[uint32]bool // peers that need relay (symmetric NAT)
beaconAddr *net.UDPAddr // beacon address for punch/relay
relayPeers map[uint32]bool // peers that need relay (symmetric NAT)

// Webhook
webhook *WebhookClient
Expand Down
1 change: 0 additions & 1 deletion pkg/driver/ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,3 @@ func (c *ipcClient) unregisterRecvCh(connID uint32) {
defer c.recvMu.Unlock()
delete(c.recvChs, connID)
}

72 changes: 58 additions & 14 deletions pkg/registry/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ header h1{font-size:20px;font-weight:600;color:#e6edf3}
header .links{display:flex;gap:16px;font-size:13px}
.uptime{font-size:12px;color:#8b949e;margin-top:4px}

.stats-row{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:32px}
.stats-row{display:grid;grid-template-columns:repeat(6,1fr);gap:16px;margin-bottom:32px}
.stat-card{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:20px;text-align:center}
.stat-card .value{font-size:32px;font-weight:700;color:#e6edf3;display:block}
.stat-card .label{font-size:12px;color:#8b949e;text-transform:uppercase;letter-spacing:0.5px;margin-top:4px}
Expand All @@ -190,9 +190,15 @@ tr:last-child td{border-bottom:none}
.tag-filter{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px 12px;color:#c9d1d9;font-family:inherit;font-size:13px;width:100%;margin-bottom:12px;outline:none}
.tag-filter:focus{border-color:#58a6ff}
.tag-filter::placeholder{color:#484f58}
.task-badge{display:inline-block;background:#1a3a2a;border:1px solid #3fb950;border-radius:12px;padding:2px 10px;font-size:11px;color:#3fb950;white-space:nowrap}
.filter-row{display:flex;gap:12px;align-items:center;margin-bottom:12px}
.filter-row .tag-filter{margin-bottom:0;flex:1}
.sort-select{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px 12px;color:#c9d1d9;font-family:inherit;font-size:13px;cursor:pointer;outline:none}
.sort-select:focus{border-color:#58a6ff}
.task-badge{display:inline-block;background:#1a3a2a;border:1px solid:#3fb950;border-radius:12px;padding:2px 10px;font-size:11px;color:#3fb950;white-space:nowrap}
.polo-score{font-weight:600;color:#f59e0b}
.polo-high{color:#3fb950}
.polo-medium{color:#58a6ff}
.polo-low{color:#8b949e}
.filter-row{display:flex;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
.filter-row .tag-filter{margin-bottom:0;flex:1;min-width:200px}
.filter-row label{font-size:13px;color:#8b949e;white-space:nowrap;cursor:pointer;display:flex;align-items:center;gap:4px}
.empty{color:#484f58;font-style:italic;padding:20px;text-align:center}

Expand Down Expand Up @@ -230,6 +236,10 @@ footer a:hover{color:#58a6ff}
<span class="value" id="total-requests">—</span>
<span class="label">Total Requests</span>
</div>
<div class="stat-card">
<span class="value" id="total-nodes">—</span>
<span class="label">Total Nodes</span>
</div>
<div class="stat-card">
<span class="value" id="active-nodes">—</span>
<span class="label">Online Nodes</span>
Expand All @@ -251,7 +261,7 @@ footer a:hover{color:#58a6ff}
<div class="section">
<h2>Networks</h2>
<table>
<thead><tr><th>ID</th><th>Name</th><th>Members</th></tr></thead>
<thead><tr><th>ID</th><th>Name</th><th>Members (Online/Total)</th></tr></thead>
<tbody id="networks-body">
<tr><td colspan="3" class="empty">Loading...</td></tr>
</tbody>
Expand All @@ -263,11 +273,19 @@ footer a:hover{color:#58a6ff}
<div class="filter-row">
<input type="text" id="tag-filter" class="tag-filter" placeholder="Filter by tag...">
<label><input type="checkbox" id="task-filter"> Tasks only</label>
<label><input type="checkbox" id="online-filter"> Online only</label>
<select id="sort-select" class="sort-select">
<option value="address">Sort by Address</option>
<option value="polo_desc">Sort by POLO Score (High-Low)</option>
<option value="polo_asc">Sort by POLO Score (Low-High)</option>
<option value="trust_desc">Sort by Trust Links (High-Low)</option>
<option value="online">Sort by Status (Online first)</option>
</select>
</div>
<table>
<thead><tr><th>Address</th><th>Status</th><th>Trust</th><th>Tags</th><th>Tasks</th></tr></thead>
<thead><tr><th>Address</th><th>Status</th><th>POLO Score</th><th>Trust</th><th>Tags</th><th>Tasks</th></tr></thead>
<tbody id="nodes-body">
<tr><td colspan="5" class="empty">Loading...</td></tr>
<tr><td colspan="6" class="empty">Loading...</td></tr>
</tbody>
</table>
<div class="pagination" id="pagination"></div>
Expand All @@ -290,11 +308,27 @@ function uptimeStr(s){var d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Mat
function getFiltered(){
var filter=document.getElementById('tag-filter').value;
var taskOnly=document.getElementById('task-filter').checked;
var onlineOnly=document.getElementById('online-filter').checked;
var sortBy=document.getElementById('sort-select').value;
var result=allNodes;
if(filter){var q=filter.toLowerCase().replace(/^#/,'');result=result.filter(function(n){return n.tags&&n.tags.some(function(t){return t.indexOf(q)>=0})})}
if(taskOnly){result=result.filter(function(n){return n.task_exec})}
if(onlineOnly){result=result.filter(function(n){return n.online})}

// Apply sorting
if(sortBy==='polo_desc'){result.sort(function(a,b){return (b.polo_score||0)-(a.polo_score||0)})}
else if(sortBy==='polo_asc'){result.sort(function(a,b){return (a.polo_score||0)-(b.polo_score||0)})}
else if(sortBy==='trust_desc'){result.sort(function(a,b){return (b.trust_links||0)-(a.trust_links||0)})}
else if(sortBy==='online'){result.sort(function(a,b){return b.online-a.online})}
else{result.sort(function(a,b){return a.address.localeCompare(b.address)})}

return result;
}
function getPoloClass(score){
if(score>=50)return 'polo-high';
if(score>=0)return 'polo-medium';
return 'polo-low';
}
function renderNodes(){
var tb=document.getElementById('nodes-body');
tb.innerHTML='';
Expand All @@ -310,14 +344,17 @@ function renderNodes(){
var td2=document.createElement('td');
var dot=document.createElement('span');dot.style.cssText='display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;background:'+(n.online?'#3fb950':'#484f58');
td2.appendChild(dot);td2.appendChild(document.createTextNode(n.online?'Online':'Offline'));td2.style.color=n.online?'#3fb950':'#484f58';
var td3=document.createElement('td');td3.textContent=n.trust_links||0;td3.style.color=n.trust_links?'#58a6ff':'#484f58';
var td4=document.createElement('td');
if(n.tags&&n.tags.length){n.tags.forEach(function(t){var s=document.createElement('span');s.className='tag';s.textContent='#'+t;td4.appendChild(s)})}else{td4.textContent='\u2014'}
var td3=document.createElement('td');
var score=n.polo_score||0;
td3.textContent=score;td3.className='polo-score '+getPoloClass(score);
var td4=document.createElement('td');td4.textContent=n.trust_links||0;td4.style.color=n.trust_links?'#58a6ff':'#484f58';
var td5=document.createElement('td');
if(n.task_exec){var b=document.createElement('span');b.className='task-badge';b.textContent='executor';td5.appendChild(b)}else{td5.textContent='\u2014'}
tr.appendChild(td1);tr.appendChild(td2);tr.appendChild(td3);tr.appendChild(td4);tr.appendChild(td5);tb.appendChild(tr);
if(n.tags&&n.tags.length){n.tags.forEach(function(t){var s=document.createElement('span');s.className='tag';s.textContent='#'+t;td5.appendChild(s)})}else{td5.textContent='\u2014'}
var td6=document.createElement('td');
if(n.task_exec){var b=document.createElement('span');b.className='task-badge';b.textContent='executor';td6.appendChild(b)}else{td6.textContent='\u2014'}
tr.appendChild(td1);tr.appendChild(td2);tr.appendChild(td3);tr.appendChild(td4);tr.appendChild(td5);tr.appendChild(td6);tb.appendChild(tr);
});
}else{tb.innerHTML='<tr><td colspan="5" class="empty">No nodes'+(document.getElementById('tag-filter').value||document.getElementById('task-filter').checked?' matching filter':' registered')+'</td></tr>'}
}else{tb.innerHTML='<tr><td colspan="6" class="empty">No nodes'+(document.getElementById('tag-filter').value||document.getElementById('task-filter').checked||document.getElementById('online-filter').checked?' matching filter':' registered')+'</td></tr>'}
var pg=document.getElementById('pagination');
if(filtered.length<=pageSize){pg.innerHTML='';return}
pg.innerHTML='';
Expand All @@ -329,6 +366,7 @@ function renderNodes(){
function update(){
fetch('/api/stats').then(function(r){return r.json()}).then(function(d){
document.getElementById('total-requests').textContent=fmt(d.total_requests);
document.getElementById('total-nodes').textContent=fmt(d.total_nodes||0);
document.getElementById('active-nodes').textContent=fmt(d.active_nodes||0);
document.getElementById('trust-links').textContent=fmt(d.total_trust_links||0);
document.getElementById('unique-tags').textContent=fmt(d.unique_tags||0);
Expand All @@ -341,7 +379,11 @@ function update(){
var tr=document.createElement('tr');
var td1=document.createElement('td');td1.textContent=n.id;
var td2=document.createElement('td');td2.textContent=n.name;
var td3=document.createElement('td');td3.textContent=n.members;
var td3=document.createElement('td');
var onlineMembers=n.online_members||0;
var totalMembers=n.members||0;
td3.textContent=onlineMembers+' / '+totalMembers;
if(onlineMembers>0){td3.style.color='#3fb950'}else{td3.style.color='#8b949e'}
tr.appendChild(td1);tr.appendChild(td2);tr.appendChild(td3);nb.appendChild(tr);
});
}else{nb.innerHTML='<tr><td colspan="3" class="empty">No networks</td></tr>'}
Expand All @@ -352,6 +394,8 @@ function update(){
}
document.getElementById('tag-filter').addEventListener('input',function(){currentPage=1;renderNodes()});
document.getElementById('task-filter').addEventListener('change',function(){currentPage=1;renderNodes()});
document.getElementById('online-filter').addEventListener('change',function(){currentPage=1;renderNodes()});
document.getElementById('sort-select').addEventListener('change',function(){currentPage=1;renderNodes()});
update();setInterval(update,30000);
</script>
</body>
Expand Down
40 changes: 26 additions & 14 deletions pkg/registry/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,14 +321,14 @@ func NewWithStore(beaconAddr, storePath string) *Server {
trustPairs: make(map[string]bool),
handshakeInbox: make(map[uint32][]*HandshakeRelayMsg),
handshakeResponses: make(map[uint32][]*HandshakeResponseMsg),
rateLimiter: NewRateLimiter(10, time.Minute), // 10 registrations per IP per minute
beacons: make(map[uint32]*beaconEntry),
replMgr: newReplicationManager(),
metrics: newRegistryMetrics(),
readyCh: make(chan struct{}),
done: make(chan struct{}),
saveCh: make(chan struct{}, 1),
saveDone: make(chan struct{}),
rateLimiter: NewRateLimiter(10, time.Minute), // 10 registrations per IP per minute
beacons: make(map[uint32]*beaconEntry),
replMgr: newReplicationManager(),
metrics: newRegistryMetrics(),
readyCh: make(chan struct{}),
done: make(chan struct{}),
saveCh: make(chan struct{}, 1),
saveDone: make(chan struct{}),
}

go s.saveLoop()
Expand Down Expand Up @@ -2430,13 +2430,15 @@ type DashboardNode struct {
Online bool `json:"online"`
TrustLinks int `json:"trust_links"`
TaskExec bool `json:"task_exec"`
PoloScore int `json:"polo_score"`
}

// DashboardNetwork is a public-safe view of a network for the dashboard.
type DashboardNetwork struct {
ID uint16 `json:"id"`
Name string `json:"name"`
Members int `json:"members"`
ID uint16 `json:"id"`
Name string `json:"name"`
Members int `json:"members"`
OnlineMembers int `json:"online_members"`
}

// DashboardEdge represents a trust relationship between two nodes.
Expand Down Expand Up @@ -2521,6 +2523,7 @@ func (s *Server) GetDashboardStats() DashboardStats {
Online: online,
TrustLinks: trustCount[node.ID],
TaskExec: node.TaskExec,
PoloScore: node.PoloScore,
})
}

Expand All @@ -2531,10 +2534,19 @@ func (s *Server) GetDashboardStats() DashboardStats {

networks := make([]DashboardNetwork, 0, len(s.networks))
for _, net := range s.networks {
onlineCount := 0
for _, memberID := range net.Members {
if node, exists := s.nodes[memberID]; exists {
if node.LastSeen.After(onlineThreshold) {
onlineCount++
}
}
}
networks = append(networks, DashboardNetwork{
ID: net.ID,
Name: net.Name,
Members: len(net.Members),
ID: net.ID,
Name: net.Name,
Members: len(net.Members),
OnlineMembers: onlineCount,
})
}

Expand Down
20 changes: 10 additions & 10 deletions tests/nat_traversal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,42 +454,42 @@ func TestNATScenarios(t *testing.T) {
description string
}{
{
name: "FullCone",
natType: "Full Cone (Endpoint Independent Mapping + Endpoint Independent Filtering)",
name: "FullCone",
natType: "Full Cone (Endpoint Independent Mapping + Endpoint Independent Filtering)",
mechanism: "direct",
description: "STUN-discovered endpoint works for all peers. " +
"Any external host can send to the mapped address:port. " +
"No hole-punching needed — direct tunnel works immediately.",
},
{
name: "RestrictedCone",
natType: "Restricted Cone (Endpoint Independent Mapping + Address Restricted Filtering)",
name: "RestrictedCone",
natType: "Restricted Cone (Endpoint Independent Mapping + Address Restricted Filtering)",
mechanism: "hole-punch",
description: "Same external port for all destinations, but NAT only allows " +
"return traffic from hosts we've sent to. Beacon coordinates simultaneous " +
"hole-punch: both sides send UDP to each other's STUN endpoint, creating " +
"the required NAT filter entries.",
},
{
name: "PortRestrictedCone",
natType: "Port Restricted Cone (Endpoint Independent Mapping + Address+Port Restricted Filtering)",
name: "PortRestrictedCone",
natType: "Port Restricted Cone (Endpoint Independent Mapping + Address+Port Restricted Filtering)",
mechanism: "hole-punch",
description: "Like restricted cone but filtering checks both address AND port. " +
"Still works with beacon hole-punching because both sides punch to the " +
"exact STUN-discovered endpoint (which uses endpoint-independent mapping).",
},
{
name: "Symmetric",
natType: "Symmetric (Endpoint Dependent Mapping)",
name: "Symmetric",
natType: "Symmetric (Endpoint Dependent Mapping)",
mechanism: "relay",
description: "Different external port for each destination. STUN port is only " +
"valid for beacon, not for peers. Hole-punching fails because port is " +
"unpredictable. Falls back to beacon relay: data is wrapped in MsgRelay " +
"and forwarded through the beacon server.",
},
{
name: "CloudVM",
natType: "No NAT (Public IP / Cloud VM)",
name: "CloudVM",
natType: "No NAT (Public IP / Cloud VM)",
mechanism: "direct",
description: "Use -endpoint flag to specify the known public IP:port. " +
"Skips STUN discovery entirely. Direct tunnel works immediately " +
Expand Down
Loading
Loading