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> {
const containerId = await findRunningDockerContainerId();
if (!containerId)
const container = await findRunningDockerContainer();
if (!container)
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 webSocketLine = logLines.find(line => line.startsWith(WS_LINE_PREFIX));
const NOVNC_LINE_PREFIX = 'novnc is listening on ';
const novncLine = logLines.find(line => line.startsWith(NOVNC_LINE_PREFIX));
return novncLine && webSocketLine ? {
wsEndpoint: 'ws://' + webSocketLine.substring(WS_LINE_PREFIX.length),
vncSession: novncLine.substring(NOVNC_LINE_PREFIX.length),
} : undefined;
if (!novncLine || !webSocketLine)
return undefined;
const wsEndpoint = containerUrlToHostUrl('ws://' + webSocketLine.substring(WS_LINE_PREFIX.length));
const vncSession = containerUrlToHostUrl(novncLine.substring(NOVNC_LINE_PREFIX.length));
return wsEndpoint && vncSession ? { wsEndpoint, vncSession } : undefined;
}
export async function ensureContainerOrDie(): Promise<ContainerInfo> {
@ -149,11 +162,11 @@ export async function ensureContainerOrDie(): Promise<ContainerInfo> {
}
export async function stopContainer() {
const containerId = await findRunningDockerContainerId();
if (!containerId)
const container = await findRunningDockerContainer();
if (!container)
return;
await dockerApi.stopContainer({
containerId,
containerId: container.containerId,
waitUntil: 'removed',
});
}
@ -176,10 +189,10 @@ async function findDockerImage(imageName: string): Promise<dockerApi.DockerImage
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 dockerImage = await findDockerImage(VRT_IMAGE_NAME);
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[];
}
export interface PortBinding {
ip: string;
hostPort: number;
containerPort: number;
}
export interface DockerContainer {
containerId: string;
imageId: string;
state: 'created'|'restarting'|'running'|'removing'|'paused'|'exited'|'dead';
names: string[];
portBindings: PortBinding[];
}
export async function listContainers(): Promise<DockerContainer[]> {
@ -38,7 +45,12 @@ export async function listContainers(): Promise<DockerContainer[]> {
containerId: container.Id,
imageId: container.ImageID,
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 = {};
for (const port of (options.ports ?? [])) {
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 : ''), {
Cmd: options.command,