mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
co-authored by
Chris DiGiamo
Chris DiGiamo
parent
8cc7f2bb0e
commit
1a6a8be96c
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user