Skip to content

Commit 40c7c57

Browse files
committed
Refactor Docker proxy host routing to avoid ExtraHosts
ExtraHosts doesn't work with any client that relies exclusively on DNS (one good example so far - Mastodon - but this is also easy to do in Node.js and elsewhere). This therefore breaks all Docker connections to the proxy on Linux, where host.docker.internal was injected this way. Instead, we now use the raw IP on Linux, avoiding DNS/hostname issues entirely.
1 parent f4f9090 commit 40c7c57

File tree

6 files changed

+27
-96
lines changed

6 files changed

+27
-96
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
OVERRIDES_DIR
1414
} from '../terminal/terminal-env-overrides';
1515
import { getDeferred } from '../../util/promise';
16-
import { DOCKER_HOST_HOSTNAME } from './docker-commands';
16+
import { getDockerHostAddress } from './docker-commands';
1717

1818
const HTTP_TOOLKIT_INJECTED_PATH = '/http-toolkit-injections';
1919
const HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH = path.posix.join(HTTP_TOOLKIT_INJECTED_PATH, 'overrides');
@@ -44,7 +44,7 @@ export function injectIntoBuildStream(
4444
{ certPath: HTTP_TOOLKIT_INJECTED_CA_PATH },
4545
'posix-runtime-inherit', // Dockerfile commands can reference vars directly
4646
{
47-
httpToolkitIp: DOCKER_HOST_HOSTNAME,
47+
httpToolkitHost: getDockerHostAddress(process.platform),
4848
overridePath: HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH,
4949
targetPlatform: 'linux'
5050
}

src/interceptors/docker/docker-commands.ts

Lines changed: 15 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -22,51 +22,28 @@ const HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH = path.posix.join(HTTP_TOOLKIT_INJECT
2222
const HTTP_TOOLKIT_INJECTED_CA_PATH = path.posix.join(HTTP_TOOLKIT_INJECTED_PATH, 'ca.pem');
2323

2424
/**
25-
* The hostname that resolves to the host OS (i.e. generally: where HTTP Toolkit is running)
25+
* Get the hostname that resolves to the host OS (i.e. generally: where HTTP Toolkit is running)
2626
* from inside containers.
2727
*
2828
* In Docker for Windows & Mac, host.docker.internal is supported automatically:
2929
* https://docs.docker.com/docker-for-windows/networking/#use-cases-and-workarounds
3030
* https://docs.docker.com/docker-for-mac/networking/#use-cases-and-workarounds
3131
*
32-
* On Linux this is _not_ supported, so we add it ourselves with (--add-host).
32+
* On Linux this is _not_ supported, and we need to be more clever.
3333
*/
34-
export const DOCKER_HOST_HOSTNAME = "host.docker.internal";
35-
36-
/**
37-
* To make the above hostname work on Linux, where it's not supported by default, we need to map it to the
38-
* host ip. This method works out the host IP to use to do so.
39-
*/
40-
export const getDockerHostIp = (
34+
export function getDockerHostAddress(
4135
platform: typeof process.platform,
42-
dockerVersion: { apiVersion: string } | { engineVersion: string },
4336
containerMetadata?: Docker.ContainerInspectInfo
44-
) => {
45-
const semverVersion = semver.coerce(
46-
'apiVersion' in dockerVersion
47-
? dockerVersion.apiVersion
48-
: dockerVersion.engineVersion
49-
);
50-
51-
if (platform !== 'linux') {
52-
// On non-linux platforms this method isn't necessary - host.docker.internal is always supported
53-
// so we can just use that.
54-
return DOCKER_HOST_HOSTNAME;
55-
} else if (
56-
semver.satisfies(
57-
semverVersion ?? '0.0.0',
58-
'apiVersion' in dockerVersion ? '>=1.41' : '>=20.10'
59-
)
60-
) {
61-
// This is supported in Docker Engine 20.10, so always supported at least in API 1.41+
62-
// Special name defined in new Docker versions, that refers to the host gateway
63-
return 'host-gateway';
64-
} else if (containerMetadata) {
65-
// Old/Unknown Linux with known container: query the metadata, and if _that_ fails, use the default gateway IP.
66-
return containerMetadata.NetworkSettings.Gateway || "172.17.0.1";
37+
) {
38+
if (platform === 'win32' || platform === 'darwin') {
39+
// On Docker Desktop, this alias always points to the host (outside the VM) IP:
40+
return 'host.docker.internal';
6741
} else {
68-
// Old/Unknown Linux without a container (e.g. during a build). Always use the default gateway IP:
69-
return "172.17.0.1";
42+
// Elsewhere (Linux) we should be able to always use the gateway address. We avoid
43+
// using ExtraHosts with host-gateway, because that uses /etc/hosts, and not all
44+
// clients use that for resolution (some use _only_ DNS lookups). IPs avoid this.
45+
return containerMetadata?.NetworkSettings.Gateway
46+
|| "172.17.0.1";
7047
}
7148
}
7249

@@ -134,7 +111,7 @@ export function transformContainerCreationConfig(
134111
{ certPath: HTTP_TOOLKIT_INJECTED_CA_PATH },
135112
envArrayToObject(currentConfig.Env),
136113
{
137-
httpToolkitIp: DOCKER_HOST_HOSTNAME,
114+
httpToolkitHost: getDockerHostAddress(process.platform),
138115
overridePath: HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH,
139116
targetPlatform: 'linux'
140117
}
@@ -160,18 +137,7 @@ export function transformContainerCreationConfig(
160137
// Bind-mount the overrides directory into the container:
161138
`${OVERRIDES_DIR}:${HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH}:ro`
162139
// ^ Both 'ro' - untrusted containers must not be able to mess with these!
163-
],
164-
...(process.platform === 'linux'
165-
// On Linux only, we need to add an explicit host to make host.docker.internal work:
166-
? {
167-
ExtraHosts: [
168-
`${DOCKER_HOST_HOSTNAME}:${proxyHost}`,
169-
// Seems that first host wins conflicts, so we go before existing values
170-
...(currentConfig.HostConfig?.ExtraHosts ?? [])
171-
]
172-
}
173-
: {}
174-
)
140+
]
175141
};
176142

177143
// Extend that config, injecting our custom overrides:
@@ -256,11 +222,7 @@ export async function restartAndInjectContainer(
256222
}
257223
});
258224

259-
const proxyHost = getDockerHostIp(
260-
process.platform,
261-
{ engineVersion: (await docker.version()).Version },
262-
containerDetails
263-
);
225+
const proxyHost = getDockerHostAddress(process.platform, containerDetails);
264226

265227
// First we clone the continer, injecting our custom settings:
266228
const newContainer = await docker.createContainer(

src/interceptors/docker/docker-networking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as mobx from 'mobx';
66

77
import { reportError } from '../../error-tracking';
88

9-
import { DOCKER_HOST_HOSTNAME, isInterceptedContainer } from './docker-commands';
9+
import { isInterceptedContainer } from './docker-commands';
1010
import { isDockerAvailable } from './docker-interception-services';
1111
import { updateDockerTunnelledNetworks } from './docker-tunnel-proxy';
1212
import { getDnsServer } from '../../dns-server';

src/interceptors/docker/docker-proxy.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ import { reportError } from '../../error-tracking';
1515
import { addShutdownHandler } from '../../shutdown';
1616

1717
import {
18+
getDockerHostAddress,
1819
isInterceptedContainer,
19-
transformContainerCreationConfig,
20-
DOCKER_HOST_HOSTNAME,
21-
getDockerHostIp
20+
transformContainerCreationConfig
2221
} from './docker-commands';
2322
import { injectIntoBuildStream, getBuildOutputPipeline } from './docker-build-injection';
2423
import { ensureDockerServicesRunning, isDockerAvailable } from './docker-interception-services';
@@ -127,10 +126,7 @@ async function createDockerProxy(proxyPort: number, httpsConfig: { certPath: str
127126
// create will fail, and will be re-run after the image is pulled in a minute.
128127
.catch(() => undefined);
129128

130-
const proxyHost = getDockerHostIp(
131-
process.platform,
132-
{ apiVersion: dockerApiVersion! },
133-
);
129+
const proxyHost = getDockerHostAddress(process.platform);
134130

135131
const transformedConfig = transformContainerCreationConfig(
136132
config,
@@ -182,18 +178,6 @@ async function createDockerProxy(proxyPort: number, httpsConfig: { certPath: str
182178

183179
requestBodyStream = streamInjection.injectedStream;
184180
extraDockerCommandCount = streamInjection.totalCommandsAddedPromise;
185-
186-
// Make sure that host.docker.internal resolves on Linux too:
187-
if (process.platform === 'linux') {
188-
reqUrl.searchParams.append(
189-
'extrahosts',
190-
`${DOCKER_HOST_HOSTNAME}:${getDockerHostIp(
191-
process.platform,
192-
{ apiVersion: dockerApiVersion! }
193-
)}`
194-
);
195-
req.url = reqUrl.toString();
196-
}
197181
}
198182

199183
const dockerReq = sendToDocker(req, requestBodyStream);

src/interceptors/docker/docker-tunnel-proxy.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as Docker from 'dockerode';
33
import * as semver from 'semver';
44
import { Mutex } from 'async-mutex';
55

6-
import { DOCKER_HOST_HOSTNAME, isImageAvailable } from './docker-commands';
6+
import { getDockerHostAddress, isImageAvailable } from './docker-commands';
77
import { isDockerAvailable } from './docker-interception-services';
88
import { delay } from '../../util/promise';
99
import { reportError } from '../../error-tracking';
@@ -73,21 +73,6 @@ export function ensureDockerTunnelRunning(proxyPort: number) {
7373
},
7474
HostConfig: {
7575
AutoRemove: true,
76-
...(process.platform === 'linux' ? {
77-
ExtraHosts: [
78-
// Make sure the host hostname is defined (not set by default on Linux).
79-
// We use the host-gateway address on engines where that's possible, or
80-
// the default Docker bridge host IP when it's not, because we're always
81-
// connected to that network.
82-
`${DOCKER_HOST_HOSTNAME}:${
83-
semver.satisfies(engineVersion, '>= 20.10')
84-
? 'host-gateway'
85-
: defaultBridgeGateway || '172.17.0.1'
86-
}`
87-
// (This doesn't reuse getDockerHostIp, since the logic is slightly
88-
// simpler and we never have container metadata/network state).
89-
]
90-
} : {}),
9176
PortBindings: {
9277
'1080/tcp': [{
9378
// Bind host-locally only: we don't want to let remote clients

src/interceptors/terminal/terminal-env-overrides.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ export function getTerminalEnvVars(
2626
| 'posix-runtime-inherit'
2727
| 'powershell-runtime-inherit',
2828
targetEnvConfig: {
29-
httpToolkitIp?: string,
29+
httpToolkitHost?: string,
3030
overridePath?: string,
3131
targetPlatform?: NodeJS.Platform
3232
} = {}
3333
): { [key: string]: string } {
34-
const { overridePath, targetPlatform, httpToolkitIp } = {
35-
httpToolkitIp: '127.0.0.1',
34+
const { overridePath, targetPlatform, httpToolkitHost } = {
35+
httpToolkitHost: '127.0.0.1',
3636
overridePath: OVERRIDES_DIR,
3737
targetPlatform: process.platform,
3838
...targetEnvConfig
@@ -52,7 +52,7 @@ export function getTerminalEnvVars(
5252
const pathVarSeparator = targetPlatform === 'win32' ? ';' : ':';
5353
const joinPath = targetPlatform === 'win32' ? path.win32.join : path.posix.join;
5454

55-
const proxyUrl = `http://${httpToolkitIp}:${proxyPort}`;
55+
const proxyUrl = `http://${httpToolkitHost}:${proxyPort}`;
5656

5757
const dockerHost = `${
5858
targetPlatform === 'win32'
@@ -73,7 +73,7 @@ export function getTerminalEnvVars(
7373

7474
const javaAgentOption = `-javaagent:"${
7575
joinPath(overridePath, JAVA_AGENT_JAR)
76-
}"=${httpToolkitIp}|${proxyPort}|${httpsConfig.certPath}`;
76+
}"=${httpToolkitHost}|${proxyPort}|${httpsConfig.certPath}`;
7777

7878
const binPath = joinPath(overridePath, BIN_OVERRIDE_DIR);
7979

0 commit comments

Comments
 (0)