feat: log tailnet tunnels to the connection log (#27423)

Co-authored-by: Chris DiGiamo <cd@anthropic.com>
Co-authored-by: Chris DiGiamo <cdigiamo@anthropic.com>
This commit is contained in:
Jon Ayers
2026-07-28 15:30:12 -05:00
committed by GitHub
co-authored by Chris DiGiamo Chris DiGiamo
parent 8cc7f2bb0e
commit 1a6a8be96c
21 changed files with 381 additions and 30 deletions
+93
View File
@@ -1367,6 +1367,9 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
})
return
}
api.logTunnelConnection(ctx, r, waws)
ctx, wsNetConn := codersdk.WebsocketNetConn(ctx, conn, websocket.MessageBinary)
defer wsNetConn.Close()
@@ -1386,6 +1389,96 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
}
}
// logTunnelConnection records a connection log entry attributing a
// tunnel to the authenticated user who opened it. Agent-reported rows
// cannot identify the user (see coderd/agentapi/connectionlog.go), and
// workspace-proxy-authenticated requests carry no API key and are
// skipped.
func (api *API) logTunnelConnection(ctx context.Context, r *http.Request, waws database.GetWorkspaceAgentAndWorkspaceByIDRow) {
apiKey, ok := httpmw.APIKeyOptional(r)
if !ok {
return
}
// Bounded so log backpressure cannot stall tunnel establishment.
writeCtx, writeCancel := context.WithTimeout(ctx, 3*time.Second)
defer writeCancel()
userAgent := r.UserAgent()
now := dbtime.Now()
// Clients re-dial automatically, so dedupe reconnects through the
// same audit session mechanism as workspace apps, keyed on
// (agent, user, IP, user agent). Status 101 and the empty slug
// keep tunnel sessions from ever colliding with app or
// port-forwarding sessions.
staleInterval := api.Options.WorkspaceAppAuditSessionTimeout
if staleInterval == 0 {
staleInterval = time.Hour
}
// nolint:gocritic // System context is needed to write audit sessions.
newSession, err := api.Database.UpsertWorkspaceAppAuditSession(dbauthz.AsSystemRestricted(writeCtx), database.UpsertWorkspaceAppAuditSessionParams{
// Config.
StaleIntervalMS: staleInterval.Milliseconds(),
// Data.
ID: uuid.New(),
AgentID: waws.WorkspaceAgent.ID,
AppID: uuid.Nil, // Tunnels are not associated with an app.
UserID: apiKey.UserID,
Ip: r.RemoteAddr,
UserAgent: userAgent,
SlugOrPort: "",
StatusCode: http.StatusSwitchingProtocols,
StartedAt: now,
UpdatedAt: now,
})
if err != nil {
// Skip logging rather than risk spamming the connection log.
api.Logger.Error(ctx, "upsert tunnel audit session",
slog.F("workspace_id", waws.WorkspaceTable.ID),
slog.F("user_id", apiKey.UserID),
slog.Error(err),
)
return
}
if !newSession {
// Reconnection of an already-logged session.
return
}
connLogger := *api.ConnectionLogger.Load()
err = connLogger.Upsert(writeCtx, database.UpsertConnectionLogParams{
ID: uuid.New(),
Time: now,
OrganizationID: waws.WorkspaceTable.OrganizationID,
WorkspaceOwnerID: waws.WorkspaceTable.OwnerID,
WorkspaceID: waws.WorkspaceTable.ID,
WorkspaceName: waws.WorkspaceTable.Name,
AgentName: waws.WorkspaceAgent.Name,
Type: database.ConnectionTypeTunnel,
IP: database.ParseIP(r.RemoteAddr),
Code: sql.NullInt32{
Int32: http.StatusSwitchingProtocols,
Valid: true,
},
UserAgent: sql.NullString{String: userAgent, Valid: userAgent != ""},
UserID: uuid.NullUUID{UUID: apiKey.UserID, Valid: true},
// Left unset so each session gets its own row; reusing peerID
// would make resume_token reconnects upsert into a stale row.
ConnectionID: uuid.NullUUID{},
ConnectionStatus: database.ConnectionStatusConnected,
// N/A
SlugOrPort: sql.NullString{},
DisconnectReason: sql.NullString{},
})
if err != nil {
api.Logger.Error(ctx, "upsert tunnel connection log",
slog.F("workspace_id", waws.WorkspaceTable.ID),
slog.F("user_id", apiKey.UserID),
slog.Error(err),
)
}
}
// handleResumeToken accepts a resume_token query parameter to use the same peer ID
func (api *API) handleResumeToken(ctx context.Context, rw http.ResponseWriter, r *http.Request) (peerID uuid.UUID, err error) {
peerID = uuid.New()