feat(docker): auto-bind container ports to host ports (#17307)

Drive-by: make sure docker container does not expose ports on `0.0.0.0`
and instead registers to localhost. This way websocket and vnc ports
are not exposed to the public internet.
This commit is contained in:
Andrey Lushnikov 2022-09-13 13:23:04 -07:00 committed by GitHub
parent 462fa7d79d
commit 705bc28e92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 39 additions and 14 deletions

View file

@ -96,18 +96,31 @@ interface ContainerInfo {
} }
export async function containerInfo(): Promise<ContainerInfo|undefined> { export async function containerInfo(): Promise<ContainerInfo|undefined> {
const containerId = await findRunningDockerContainerId(); const container = await findRunningDockerContainer();
if (!containerId) if (!container)
return undefined; return undefined;
const logLines = await dockerApi.getContainerLogs(containerId); const logLines = await dockerApi.getContainerLogs(container.containerId);
const containerUrlToHostUrl = (address: string) => {
const url = new URL(address);
const portBinding = container.portBindings.find(binding => binding.containerPort === +url.port);
if (!portBinding)
return undefined;
url.host = portBinding.ip;
url.port = portBinding.hostPort + '';
return url.toString();
};
const WS_LINE_PREFIX = 'Listening on ws://'; const WS_LINE_PREFIX = 'Listening on ws://';
const webSocketLine = logLines.find(line => line.startsWith(WS_LINE_PREFIX)); const webSocketLine = logLines.find(line => line.startsWith(WS_LINE_PREFIX));
const NOVNC_LINE_PREFIX = 'novnc is listening on '; const NOVNC_LINE_PREFIX = 'novnc is listening on ';
const novncLine = logLines.find(line => line.startsWith(NOVNC_LINE_PREFIX)); const novncLine = logLines.find(line => line.startsWith(NOVNC_LINE_PREFIX));
return novncLine && webSocketLine ? { if (!novncLine || !webSocketLine)
wsEndpoint: 'ws://' + webSocketLine.substring(WS_LINE_PREFIX.length), return undefined;
vncSession: novncLine.substring(NOVNC_LINE_PREFIX.length), const wsEndpoint = containerUrlToHostUrl('ws://' + webSocketLine.substring(WS_LINE_PREFIX.length));
} : undefined; const vncSession = containerUrlToHostUrl(novncLine.substring(NOVNC_LINE_PREFIX.length));
return wsEndpoint && vncSession ? { wsEndpoint, vncSession } : undefined;
} }
export async function ensureContainerOrDie(): Promise<ContainerInfo> { export async function ensureContainerOrDie(): Promise<ContainerInfo> {
@ -149,11 +162,11 @@ export async function ensureContainerOrDie(): Promise<ContainerInfo> {
} }
export async function stopContainer() { export async function stopContainer() {
const containerId = await findRunningDockerContainerId(); const container = await findRunningDockerContainer();
if (!containerId) if (!container)
return; return;
await dockerApi.stopContainer({ await dockerApi.stopContainer({
containerId, containerId: container.containerId,
waitUntil: 'removed', waitUntil: 'removed',
}); });
} }
@ -176,10 +189,10 @@ async function findDockerImage(imageName: string): Promise<dockerApi.DockerImage
return images.find(image => image.names.includes(imageName)); return images.find(image => image.names.includes(imageName));
} }
async function findRunningDockerContainerId(): Promise<string|undefined> { async function findRunningDockerContainer(): Promise<dockerApi.DockerContainer|undefined> {
const containers = await dockerApi.listContainers(); const containers = await dockerApi.listContainers();
const dockerImage = await findDockerImage(VRT_IMAGE_NAME); const dockerImage = await findDockerImage(VRT_IMAGE_NAME);
const container = dockerImage ? containers.find(container => container.imageId === dockerImage.imageId) : undefined; const container = dockerImage ? containers.find(container => container.imageId === dockerImage.imageId) : undefined;
return container?.state === 'running' ? container.containerId : undefined; return container?.state === 'running' ? container : undefined;
} }

View file

@ -25,11 +25,18 @@ export interface DockerImage {
names: string[]; names: string[];
} }
export interface PortBinding {
ip: string;
hostPort: number;
containerPort: number;
}
export interface DockerContainer { export interface DockerContainer {
containerId: string; containerId: string;
imageId: string; imageId: string;
state: 'created'|'restarting'|'running'|'removing'|'paused'|'exited'|'dead'; state: 'created'|'restarting'|'running'|'removing'|'paused'|'exited'|'dead';
names: string[]; names: string[];
portBindings: PortBinding[];
} }
export async function listContainers(): Promise<DockerContainer[]> { export async function listContainers(): Promise<DockerContainer[]> {
@ -38,7 +45,12 @@ export async function listContainers(): Promise<DockerContainer[]> {
containerId: container.Id, containerId: container.Id,
imageId: container.ImageID, imageId: container.ImageID,
state: container.State, state: container.State,
names: container.Names names: container.Names,
portBindings: container.Ports?.map((portInfo: any) => ({
ip: portInfo.IP,
hostPort: portInfo.PublicPort,
containerPort: portInfo.PrivatePort,
})) ?? [],
})); }));
} }
@ -56,7 +68,7 @@ export async function launchContainer(options: LaunchContainerOptions): Promise<
const PortBindings: any = {}; const PortBindings: any = {};
for (const port of (options.ports ?? [])) { for (const port of (options.ports ?? [])) {
ExposedPorts[`${port}/tcp`] = {}; ExposedPorts[`${port}/tcp`] = {};
PortBindings[`${port}/tcp`] = [{ HostPort: port + '' }]; PortBindings[`${port}/tcp`] = [{ HostPort: '0', HostIp: '127.0.0.1' }];
} }
const container = await postJSON(`/containers/create` + (options.name ? '?name=' + options.name : ''), { const container = await postJSON(`/containers/create` + (options.name ? '?name=' + options.name : ''), {
Cmd: options.command, Cmd: options.command,