Skip to content

Commit a9afe82

Browse files
committed
Inject content into Docker via volumes, not bind mounts
This is useful, because a) bind mounts are slow and problematic (they come with various warnings) on Docker Desktop on Windows & Mac) and b) this moves us slowly towards a world where we can successfully intercept a _remote_ Docker host (though we still can't yet, mostly because we don't know the right proxy address to use for HTTP Toolkit). Note that unlike other Docker services, we ensure the Docker volume is running synchronously on-demand, not just async when we see Docker proxy activity. Without that, if the volume didn't exist for some reason (shouldn't happen, but possible given races etc or deletion elsewhere) then containers could've been created referencing it when it didn't exist, whereupon Docker would create it automatically and break everything.
1 parent d45f04c commit a9afe82

File tree

8 files changed

+257
-96
lines changed

8 files changed

+257
-96
lines changed

src/api-server.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,19 @@ import { GraphQLScalarType } from 'graphql';
99
import { graphqlHTTP } from 'express-graphql';
1010
import gql from 'graphql-tag';
1111

12-
import { generateSPKIFingerprint, MockttpAdminServer } from 'mockttp';
12+
import { generateSPKIFingerprint } from 'mockttp';
1313
import { getSystemProxy } from 'os-proxy-config';
1414

1515
import { HtkConfig } from './config';
1616
import { reportError, addBreadcrumb } from './error-tracking';
1717
import { buildInterceptors, Interceptor, ActivationError } from './interceptors';
18-
import { ALLOWED_ORIGINS } from './constants';
18+
import { ALLOWED_ORIGINS, SERVER_VERSION } from './constants';
1919
import { delay } from './util/promise';
2020
import { getDnsServer } from './dns-server';
2121
import { shutdown } from './shutdown';
2222

2323
const ENABLE_PLAYGROUND = false;
2424

25-
const packageJson = require('../package.json');
26-
2725
/**
2826
* This file contains the core server API, used by the UI to query
2927
* machine state that isn't easily visible from the web (cert files,
@@ -119,7 +117,7 @@ const buildResolvers = (
119117
) => {
120118
return {
121119
Query: {
122-
version: () => packageJson.version,
120+
version: () => SERVER_VERSION,
123121
interceptors: () => _.values(interceptors),
124122
interceptor: (_: any, { id } : { id: string }) => interceptors[id],
125123
config: () => ({

src/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,6 @@ export const MOCKTTP_ALLOWED_ORIGINS = [
3535
// certificate check server and Chrome's "hide warning" server. These ports are extra likely
3636
// not to conflict with normal user usage, and are specifically designated by the IANA for
3737
// use for dynamic ports.
38-
export const EPHEMERAL_PORT_RANGE = { startPort: 49152, endPort: 65535 } as const;
38+
export const EPHEMERAL_PORT_RANGE = { startPort: 49152, endPort: 65535 } as const;
39+
40+
export const SERVER_VERSION = require('../package.json').version as string;

src/interceptors/docker/docker-build-injection.ts

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@ import * as EventStream from 'event-stream';
55
import * as getRawBody from 'raw-body';
66
import maybeGunzip = require('gunzip-maybe');
77
import * as tarStream from 'tar-stream';
8-
import * as tarFs from 'tar-fs';
98
import { parse as parseDockerfile, CommandEntry } from 'docker-file-parser';
109

11-
import {
12-
getTerminalEnvVars,
13-
OVERRIDES_DIR
14-
} from '../terminal/terminal-env-overrides';
10+
import { getTerminalEnvVars } from '../terminal/terminal-env-overrides';
1511
import { getDeferred } from '../../util/promise';
1612
import { getDockerHostAddress } from './docker-commands';
13+
import { packOverrideFiles } from './docker-data-injection';
1714

1815
const HTTP_TOOLKIT_INJECTED_PATH = '/http-toolkit-injections';
1916
const HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH = path.posix.join(HTTP_TOOLKIT_INJECTED_PATH, 'overrides');
@@ -74,7 +71,7 @@ export function injectIntoBuildStream(
7471
});
7572
extractionStream.on('finish', async () => {
7673
repackStream.entry({ name: HTTP_TOOLKIT_CONTEXT_CA_PATH }, config.certContent);
77-
await packOverrideFiles(repackStream);
74+
await packOverrideFiles(repackStream, HTTP_TOOLKIT_CONTEXT_OVERRIDES_PATH);
7875
repackStream.finalize();
7976
});
8077

@@ -91,28 +88,6 @@ export function injectIntoBuildStream(
9188
};
9289
}
9390

94-
function packOverrideFiles(existingPackStream: tarStream.Pack) {
95-
return new Promise<tarStream.Pack>((resolve) => {
96-
tarFs.pack(OVERRIDES_DIR, {
97-
pack: existingPackStream,
98-
map: (fileHeader) => {
99-
fileHeader.name = path.posix.join(HTTP_TOOLKIT_CONTEXT_OVERRIDES_PATH, fileHeader.name);
100-
101-
// Owned by root by default
102-
fileHeader.uid = 0;
103-
fileHeader.gid = 0;
104-
105-
// But ensure everything is globally readable & runnable
106-
fileHeader.mode = parseInt('555', 8);
107-
108-
return fileHeader;
109-
},
110-
finalize: false,
111-
finish: resolve
112-
});
113-
});
114-
}
115-
11691
// Simplify to just the params we care about
11792
type DockerCommand = Pick<CommandEntry, 'name' | 'args'> & { raw?: string };
11893

src/interceptors/docker/docker-commands.ts

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import * as _ from 'lodash';
22
import * as Docker from 'dockerode';
33
import * as path from 'path';
4-
import * as semver from 'semver';
54

6-
import {
7-
getTerminalEnvVars,
8-
OVERRIDES_DIR
9-
} from '../terminal/terminal-env-overrides';
5+
import { getTerminalEnvVars } from '../terminal/terminal-env-overrides';
106
import { transformComposeCreationLabels } from './docker-compose';
7+
import { getDockerDataVolumeName } from './docker-data-injection';
118

129
// Used to label intercepted docker containers with the port of the proxy
1310
// that's currently intercepting them.
@@ -76,15 +73,14 @@ const envObjectToArray = (envObject: { [key: string]: string }): string[] =>
7673
* the container (to make sure we get *all* env vars, for example) and then
7774
* combine that with the inter
7875
*/
79-
export function transformContainerCreationConfig(
76+
export async function transformContainerCreationConfig(
8077
containerConfig: Docker.ContainerCreateOptions,
8178
baseImageConfig: Docker.ImageInspectInfo | undefined,
82-
{ proxyPort, proxyHost, certPath }: {
79+
{ proxyPort, certContent }: {
8380
proxyPort: number,
84-
proxyHost: string,
85-
certPath: string
81+
certContent: string
8682
}
87-
): Docker.ContainerCreateOptions {
83+
): Promise<Docker.ContainerCreateOptions> {
8884
// Get the container-relevant config from the image config first.
8985
// The image has both .Config and .ContainerConfig. The former
9086
// is preferred, seems that .ContainerConfig is backward compat.
@@ -127,16 +123,13 @@ export function transformContainerCreationConfig(
127123
// files into place on top of the existing content:
128124
Binds: [
129125
...(currentConfig.HostConfig?.Binds ?? []).filter((existingMount) =>
130-
// Drop any existing mounts for these folders - this allows re-intercepting containers, e.g.
131-
// to switch from one proxy port to another.
132-
!existingMount.startsWith(`${certPath}:`) &&
133-
!existingMount.startsWith(`${OVERRIDES_DIR}:`)
126+
// Drop any existing mounts for these folders - this allows re-intercepting containers,
127+
// e.g. to switch from one proxy port to another.
128+
!existingMount.endsWith(`:${HTTP_TOOLKIT_INJECTED_PATH}:ro`)
134129
),
135-
// Bind-mount the CA certificate file individually too:
136-
`${certPath}:${HTTP_TOOLKIT_INJECTED_CA_PATH}:ro`,
137-
// Bind-mount the overrides directory into the container:
138-
`${OVERRIDES_DIR}:${HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH}:ro`
139-
// ^ Both 'ro' - untrusted containers must not be able to mess with these!
130+
// Bind-mount the injected data volume:
131+
`${await getDockerDataVolumeName(certContent)}:${HTTP_TOOLKIT_INJECTED_PATH}:ro`,
132+
// ^ Note the 'ro' - untrusted containers must not be able to mess with this!
140133
]
141134
};
142135

@@ -195,10 +188,9 @@ async function connectNetworks(
195188
export async function restartAndInjectContainer(
196189
docker: Docker,
197190
containerId: string,
198-
{ proxyPort, certContent, certPath }: {
191+
{ proxyPort, certContent }: {
199192
proxyPort: number,
200193
certContent: string
201-
certPath: string
202194
}
203195
) {
204196
// We intercept containers by stopping them, cloning them, injecting our settings,
@@ -222,22 +214,16 @@ export async function restartAndInjectContainer(
222214
}
223215
});
224216

225-
const proxyHost = getDockerHostAddress(process.platform, containerDetails);
226-
227217
// First we clone the continer, injecting our custom settings:
228218
const newContainer = await docker.createContainer(
229-
transformContainerCreationConfig(
219+
await transformContainerCreationConfig(
230220
// Get options required to directly recreate this container
231221
deriveContainerCreationConfigFromInspection(
232222
containerDetails
233223
),
234224
// We don't need image config - inspect result has *everything*
235225
undefined,
236-
{ // The settings to inject:
237-
certPath,
238-
proxyPort,
239-
proxyHost
240-
}
226+
{ proxyPort, certContent }
241227
)
242228
);
243229

0 commit comments

Comments
 (0)