From b02eb7c0e439bb127037fb882e24e88fb6607c26 Mon Sep 17 00:00:00 2001 From: Wojciech Nawrocki Date: Thu, 7 May 2026 16:20:25 -0400 Subject: [PATCH 1/5] feat: auth_request --- src/app/api/auth-vsc/[sessionId]/route.ts | 11 +++++++++++ src/lib/server/editorSessions.ts | 9 +++++++++ src/lib/server/vscodeServer.ts | 21 ++++++++++++--------- 3 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 src/app/api/auth-vsc/[sessionId]/route.ts diff --git a/src/app/api/auth-vsc/[sessionId]/route.ts b/src/app/api/auth-vsc/[sessionId]/route.ts new file mode 100644 index 0000000..a7ecaed --- /dev/null +++ b/src/app/api/auth-vsc/[sessionId]/route.ts @@ -0,0 +1,11 @@ +import { requireAuth } from '@/lib/server/actions' +import { getEditorSessionManager } from '@/lib/server/editorSessions' +import { forbidden } from 'next/navigation' + +/** Queried by Nginx to ensure the sending user can access the given editor session. */ +export async function GET(_req: Request, { params }: { params: Promise<{ sessionId: string }> }) { + const { sessionId } = await params + const userSession = await requireAuth() + if (!getEditorSessionManager().isViewerOf(userSession.user.id, sessionId)) forbidden() + return new Response(null, { status: 200 }) +} diff --git a/src/lib/server/editorSessions.ts b/src/lib/server/editorSessions.ts index 0ba6d82..9d24c09 100644 --- a/src/lib/server/editorSessions.ts +++ b/src/lib/server/editorSessions.ts @@ -129,6 +129,15 @@ export class EditorSessionManager { void session.dispose() } + /** Returns true iff `userId` is the viewer of the session `sessionId`. */ + isViewerOf(userId: string, sessionId: string): boolean { + for (const servers of this.vscServers.values()) { + const s = servers.find(s => s.uuid === sessionId) + if (s) return s.viewer.id === userId + } + return false + } + async listSessions(): Promise { const result: EditorSessionInfo[] = [] for (const [projectId, servers] of this.vscServers) { diff --git a/src/lib/server/vscodeServer.ts b/src/lib/server/vscodeServer.ts index f268560..c2d0965 100644 --- a/src/lib/server/vscodeServer.ts +++ b/src/lib/server/vscodeServer.ts @@ -57,7 +57,9 @@ async function waitForNginxRoute(path: string, timeoutMs = 10_000): Promise resolve(null)) req.end() }) - if (status !== null && status === 200) return + // The route is gated by `auth_request`, + // so this unauthenticated probe should return 401 once the route is ready. + if (status !== null && status === 401) return await new Promise(r => setTimeout(r, 100)) } throw new Error(`Timeout waiting for Nginx route ${path} (last HTTP status=${status})`) @@ -102,14 +104,15 @@ export class VscodeServerHandle { private async writeNginxUserRoute() { const conf = `location ${this.vscodeIframePath} { - proxy_pass http://127.0.0.1:${this.port}; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $http_host; - proxy_buffering off; - proxy_read_timeout 86400; - proxy_hide_header X-Frame-Options; + auth_request /api/auth-vsc/${this.uuid}; + proxy_pass http://127.0.0.1:${this.port}; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + proxy_buffering off; + proxy_read_timeout 86400; + proxy_hide_header X-Frame-Options; } ` await fs.writeFile(this.nginxUserRoutePath, conf) From 325cb5bbc8500011f548114a691aabd508f3b3f3 Mon Sep 17 00:00:00 2001 From: Wojciech Nawrocki Date: Thu, 7 May 2026 17:24:23 -0400 Subject: [PATCH 2/5] fix: websocket auth --- nginx.conf.template | 18 +++++++++++++++++- src/lib/server/util.ts | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/nginx.conf.template b/nginx.conf.template index 37797b6..be2e29c 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -13,6 +13,7 @@ http { sendfile on; + # See https://nginx.org/en/docs/http/websocket.html. map $http_upgrade $connection_upgrade { default upgrade; '' close; @@ -24,12 +25,27 @@ http { # Dynamic per-user routes (written by main server) include $NGINX_CONF_DIR/user-routes/*.conf; - + # Block unmatched /_vs/* — only allow user routes installed above location /_vs/ { return 404; } + # Target of `auth_request` in VSCode user routes. + # See https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/. + # Mostly identical to the blanket forwarding route below, + # but importantly skips Upgrade/Connection headers: + # WebSocket connections to VSCode are broken if we forward these. + location /api/auth-vsc/ { + # Can only be reached by internal requests + internal; + proxy_pass http://127.0.0.1:3002; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + } + # Everything else -> main server location / { proxy_pass http://127.0.0.1:3002; diff --git a/src/lib/server/util.ts b/src/lib/server/util.ts index 92d928e..cb71c48 100644 --- a/src/lib/server/util.ts +++ b/src/lib/server/util.ts @@ -49,6 +49,7 @@ export const BWRAP_ARGS = '--unshare-cgroup', // TODO(security): unshare-net but allow outgoing inet connections for VSC bwraps. // https://github.com/containers/bubblewrap/issues/504 + // https://github.com/rootless-containers/slirp4netns '--die-with-parent', '--new-session', '--clearenv', From d8ff9c099171c12c22011396b8d4f3b45144d6f0 Mon Sep 17 00:00:00 2001 From: Wojciech Nawrocki Date: Thu, 7 May 2026 18:32:00 -0400 Subject: [PATCH 3/5] chore: silence auth-vsc log --- next.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/next.config.ts b/next.config.ts index a548b3c..abda98f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -14,6 +14,12 @@ const nextConfig: NextConfig = { images: { remotePatterns: [{ protocol: 'https', hostname: 'avatars.githubusercontent.com', pathname: '/u/**' }], }, + logging: { + incomingRequests: { + // Noisy route - VSC makes a lot of requests. + ignore: [/^\/api\/auth-vsc\//], + }, + }, } export default nextConfig From f0d226a0936973b4c2f5e488ab26770b0a1cc22f Mon Sep 17 00:00:00 2001 From: Wojciech Nawrocki Date: Thu, 7 May 2026 18:45:31 -0400 Subject: [PATCH 4/5] feat: place VSC on UDS --- nginx.conf.template | 2 ++ src/lib/server/collabServer.ts | 6 ++++-- src/lib/server/vscodeServer.ts | 22 +++++++++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/nginx.conf.template b/nginx.conf.template index be2e29c..8b4c585 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -2,6 +2,8 @@ worker_processes auto; daemon off; pid $NGINX_PID_PATH; error_log $NGINX_ERROR_LOG_PATH; +# Run worker processes as root so that they can access root-owned Unix domain sockets. +user root; events { worker_connections 1024; diff --git a/src/lib/server/collabServer.ts b/src/lib/server/collabServer.ts index 9227d15..7049774 100644 --- a/src/lib/server/collabServer.ts +++ b/src/lib/server/collabServer.ts @@ -54,13 +54,15 @@ export class CollabServerHandle { // prettier-ignore [ ...BWRAP_ARGS, + // We don't need internet access. + '--unshare-net', '--ro-bind', getCollabServerDir(), '/workspace/.collab-server', - '--bind', this.socketDir, '/workspace/.collab-sockets', + '--bind', this.socketDir, '/workspace/.sockets/collab-server', // Mount project files as writable for the collaboration server. '--bind', this.projectDir, '/workspace/project', '/usr/bin/node', '/workspace/.collab-server/server.ts', - `/workspace/.collab-sockets/${COLLAB_SOCKET_FILENAME}`, + `/workspace/.sockets/collab-server/${COLLAB_SOCKET_FILENAME}`, '/workspace/project', ], { stdio: 'inherit' }, diff --git a/src/lib/server/vscodeServer.ts b/src/lib/server/vscodeServer.ts index c2d0965..1f9134a 100644 --- a/src/lib/server/vscodeServer.ts +++ b/src/lib/server/vscodeServer.ts @@ -65,6 +65,9 @@ async function waitForNginxRoute(path: string, timeoutMs = 10_000): Promise { + await fs.rm(this.socketDir, { recursive: true, force: true }) + }) + // Every user gets their own VSCode server configuration, and set of installed extensions. // Openvscode-server derives --user-data-dir and --extensions-dir from --server-data-dir: // https://github.com/gitpod-io/openvscode-server/blob/2bfb814c5215c51a10e80c2cb1b58ed91068ad8b/src/vs/server/node/server.main.ts @@ -193,7 +201,8 @@ export class VscodeServerHandle { // but users can still write files directly if needed. // Lake and other CLI tools do such writes. '--bind', this.projectDir, sandboxProjectDir, - '--bind', this.collabSocketDir, '/workspace/.collab-sockets', + '--bind', this.collabSocketDir, '/workspace/.sockets/collab-server', + '--bind', this.socketDir, '/workspace/.sockets/openvscode-server', ...overlayArgs, '--setenv', 'HOME', '/workspace', '--setenv', 'ELAN_HOME', '/workspace/.elan', @@ -208,8 +217,7 @@ export class VscodeServerHandle { ...devArgs, '--', '/workspace/.openvscode-server/bin/openvscode-server', - '--host', '127.0.0.1', - '--port', String(this.port), + '--socket-path', `/workspace/.sockets/openvscode-server/${VSCODE_SOCKET_FILENAME}`, '--without-connection-token', `--server-base-path=${this.vscodeIframePath}`, '--server-data-dir', '/workspace/.vscode-remote', From 266a218e2decb51ef119b1286ad783e7a124613a Mon Sep 17 00:00:00 2001 From: Wojciech Nawrocki Date: Thu, 7 May 2026 18:55:10 -0400 Subject: [PATCH 5/5] chore: replace port code --- src/app/admin/components/SessionRow.tsx | 2 +- src/lib/server/editorSessions.ts | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/app/admin/components/SessionRow.tsx b/src/app/admin/components/SessionRow.tsx index a09b197..977151a 100644 --- a/src/app/admin/components/SessionRow.tsx +++ b/src/app/admin/components/SessionRow.tsx @@ -23,7 +23,7 @@ export function SessionRow({ info }: { info: EditorSessionInfo }) { {info.projectName}
- port {info.port} + UUID {info.sessionId} diff --git a/src/lib/server/editorSessions.ts b/src/lib/server/editorSessions.ts index 9d24c09..439c7c6 100644 --- a/src/lib/server/editorSessions.ts +++ b/src/lib/server/editorSessions.ts @@ -16,12 +16,8 @@ export interface EditorSessionInfo { ownerUsername: string projectId: string projectName: string - port: number } -const BASE_PORT = 3010 -const MAX_PORT = 3999 - export class EditorSessionManager { /** projectId ↦ [servers for that project] * @@ -42,8 +38,6 @@ export class EditorSessionManager { * Same invariant as in {@link vscServers}. */ private collabServers = new Map() - private availablePorts = new Set(Array.from({ length: MAX_PORT - BASE_PORT + 1 }, (_, i) => BASE_PORT + i)) - constructor() { this.vscServerEvents.addListener('close', s => { if ((this.vscServers.get(s.project.id) ?? []).length === 0) { @@ -81,20 +75,15 @@ export class EditorSessionManager { const projectSessions = this.vscServers.get(project.id) ?? [] let session = projectSessions.find(s => s.viewer.id === viewer.id) if (!session) { - const port = this.availablePorts.values().next().value - if (port === undefined) throw new Error('no available ports') - this.availablePorts.delete(port) - const projectDir = path.join(getWorkspacesDir(), owner.name, project.id) const collabServer = this.findCollabServer(project, projectDir) - session = new VscodeServerHandle(viewer, owner, project, projectDir, collabServer.socketDir, port) + session = new VscodeServerHandle(viewer, owner, project, projectDir, collabServer.socketDir) session.addDisposable(async () => { this.vscServers.set( project.id, (this.vscServers.get(project.id) ?? []).filter(s => s !== session), ) this.vscServerEvents.emit('close', session!) - this.availablePorts.add(port) }) // Insertion happens in same transaction as failed lookup (before any `await`) this.vscServers.set(project.id, [...(this.vscServers.get(project.id) ?? []), session]) @@ -154,7 +143,6 @@ export class EditorSessionManager { ownerUsername: project.user.name, projectId, projectName: project.name, - port: s.port, }) } }