Skip to content

Commit 483eee6

Browse files
author
Developer
committed
fix: keep agent connections alive and auto-register connected agents
Two related bugs left agents permanently offline in the UI: 1. The hub set a 30s read deadline but refreshed it only on pong frames, while agents heartbeat with pings (client.go pingInterval). Gorilla auto-responds to pings but never refreshed the deadline, so every agent connection died at 30s and reconnected forever. Handle pings instead and relax the deadline to the agent's 90s pongWait. 2. The agent store is in-memory, so a server restart drops every registration, and the OnConnect hook only updated existing records without creating missing ones. A restart therefore made the real agent invisible in the UI (and unselectable in PITR), pushing users to create fake agents manually. OnConnect now auto-registers any agent presenting a CA-signed mTLS certificate under the first org.
1 parent 8a34d48 commit 483eee6

3 files changed

Lines changed: 52 additions & 4 deletions

File tree

internal/server/org/store.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Invite struct {
3535
type OrgStore interface {
3636
Create(org *Organization) error
3737
Get(id string) (*Organization, error)
38+
ListAll() ([]*Organization, error)
3839
ListByUserID(userID string) ([]*Organization, error)
3940
AddMember(orgID, userID, role string) error
4041
RemoveMember(orgID, userID string) error
@@ -88,6 +89,21 @@ func (s *InMemoryOrgStore) Get(id string) (*Organization, error) {
8889
return org, nil
8990
}
9091

92+
// ListAll returns all organisations.
93+
func (s *InMemoryOrgStore) ListAll() ([]*Organization, error) {
94+
s.mu.RLock()
95+
defer s.mu.RUnlock()
96+
97+
var orgs []*Organization
98+
for _, o := range s.orgs {
99+
orgs = append(orgs, o)
100+
}
101+
if orgs == nil {
102+
return []*Organization{}, nil
103+
}
104+
return orgs, nil
105+
}
106+
91107
// ListByUserID returns all organisations the given user is a member of.
92108
func (s *InMemoryOrgStore) ListByUserID(userID string) ([]*Organization, error) {
93109
s.mu.RLock()

internal/server/server.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,36 @@ func New() (*Server, error) {
6969
pitrHandler := pitr.NewHandler(pitrStore, agentStore, orgStore, auditStore, jwtSecret(), agentHub)
7070
auditHandler := audit.NewHandler(auditStore, orgStore, jwtSecret())
7171

72-
// Keep the agents API's status field in sync with the hub.
72+
// Keep the agents API's status field in sync with the hub. Unknown agents
73+
// (e.g. after a server restart cleared the in-memory store) are registered
74+
// automatically so they show up in the UI and are selectable in PITR.
7375
agentHub.SetLifecycleHooks(hub.LifecycleHooks{
7476
OnConnect: func(agentID string) {
7577
if rec, err := agentStore.Get(agentID); err == nil {
7678
rec.Status = "online"
7779
rec.LastSeen = time.Now()
7880
_ = agentStore.Update(rec)
81+
return
82+
}
83+
84+
// Presenting a CA-signed mTLS certificate proves registration, so
85+
// trust it and auto-register the agent under the first org.
86+
orgs, _ := orgStore.ListAll()
87+
var orgID string
88+
if len(orgs) > 0 {
89+
orgID = orgs[0].ID
90+
}
91+
rec := &agent.AgentRecord{
92+
ID: agentID,
93+
OrgID: orgID,
94+
Hostname: agentID,
95+
Status: "online",
96+
LastSeen: time.Now(),
97+
CreatedAt: time.Now(),
98+
Approved: true,
99+
}
100+
if err := agentStore.Create(rec); err == nil {
101+
log.Printf("server: auto-registered connected agent %s", agentID)
79102
}
80103
},
81104
OnDisconnect: func(agentID string) {

internal/ws/hub/hub.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import (
1414
)
1515

1616
const (
17-
readDeadline = 30 * time.Second
17+
// readDeadline matches the agent's pongWait (90s): the agent heartbeats
18+
// with a ping every 30s (client.go pingInterval), so 90s gives three
19+
// missed heartbeats before we declare the connection dead.
20+
readDeadline = 90 * time.Second
1821
writeTimeout = 10 * time.Second
1922
)
2023

@@ -131,8 +134,14 @@ func (h *Hub) HandleConnection(conn *gorilla.Conn, r *http.Request) {
131134

132135
log.Printf("hub: agent %s connected", agentID)
133136

134-
conn.SetPongHandler(func(string) error {
135-
return conn.SetReadDeadline(time.Now().Add(readDeadline))
137+
// The agent heartbeats by sending a ping every 30s; it never sends pong
138+
// frames itself. Refresh our read deadline on each ping and reply with a
139+
// pong. The default gorilla PingHandler would reply automatically but
140+
// would not refresh the deadline, which previously killed every
141+
// connection at readDeadline.
142+
conn.SetPingHandler(func(appData string) error {
143+
_ = conn.SetReadDeadline(time.Now().Add(readDeadline))
144+
return conn.WriteControl(gorilla.PongMessage, []byte(appData), time.Now().Add(writeTimeout))
136145
})
137146

138147
if err := conn.SetReadDeadline(time.Now().Add(readDeadline)); err != nil {

0 commit comments

Comments
 (0)