Skip to content

Commit f4f9090

Browse files
committed
Improve handling of Docker host-gateway routing
Previously, we explicitly handled the host.docker.internal alias, treating it as non-routeable (so that it was handled locally, and the mapping to 127.0.0.1 went to the host). This was tied to the specific alias, and didn't support other EXTRA_HOSTS mappings. This is now expanded, with custom mappings and other standard aliases also included. This also reduces our dependency on host.docker.internal generally, which we're going to move away from for proxy config imminently.
1 parent 2747cb9 commit f4f9090

File tree

4 files changed

+65
-16
lines changed

4 files changed

+65
-16
lines changed

src/interceptors/docker/docker-networking.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ function combineSets<T>(...sets: ReadonlySet<T>[]): ReadonlySet<T> {
151151
return new Set(result);
152152
}
153153

154-
function combineSetMaps<T>(...setMaps: Array<{ [key: string]: ReadonlySet<T> }>): { [key: string]: ReadonlySet<T> } {
154+
function combineSetMaps<T>(...setMaps: Array<{ [key: string]: ReadonlySet<T> }>): {
155+
[key: string]: ReadonlySet<T>
156+
} {
155157
const keys = _.uniq(_.flatMap(setMaps, (mapping) => Object.keys(mapping)));
156158

157159
return _.fromPairs(
@@ -163,6 +165,12 @@ function combineSetMaps<T>(...setMaps: Array<{ [key: string]: ReadonlySet<T> }>)
163165
);
164166
}
165167

168+
// We treat host gateway routes totally separately to other routes. They resolve to 127.0.0.1,
169+
// but from the *host* POV (not the container/tunnel) so we have to handle them separately.
170+
const HostGateway = Symbol('host-gateway');
171+
type HostGateway = typeof HostGateway;
172+
const HostGatewaySet = new Set<HostGateway>([HostGateway]);
173+
166174
/**
167175
* Network monitors tracks which networks the intercepted containers are connected to, and
168176
* monitors the network aliases & IPs accessible on those networks.
@@ -192,7 +200,7 @@ class DockerNetworkMonitor {
192200

193201
private readonly networkTargets: {
194202
[networkId: string]: {
195-
[hostname: string]: ReadonlySet<string>
203+
[hostname: string]: ReadonlySet<string | HostGateway>
196204
}
197205
} = mobx.observable({});
198206

@@ -206,20 +214,34 @@ class DockerNetworkMonitor {
206214
return new Set([
207215
..._.flatten(
208216
Object.values(this.networkTargets)
209-
.map((networkMap) => Object.keys(networkMap))
210-
).filter((host) =>
211-
// We don't reroute the host hostname - the host is accessible from the host already
212-
host !== DOCKER_HOST_HOSTNAME
217+
.map((networkMap) =>
218+
Object.entries(networkMap)
219+
// Exclude any aliases that might map to the host itself:
220+
.filter(([_alias, targets]) => ![...targets].some(t => t === HostGateway))
221+
.map(([alias]) => alias)
222+
)
213223
)
214224
]);
215225
}
216226

217-
// The list of mappings per-network, binding aliases to their (0+) target IPs
227+
// The list of mappings per-network, binding aliases to their (0+) target IPs.
228+
// For aliases returned by dockerRoutedAliases, this should be the tunnel-relative
229+
// IP. For other aliases, this should be host-relative.
218230
get aliasIpMap(): { [host: string]: ReadonlySet<string> } {
219-
return combineSetMaps(...Object.values(this.networkTargets), {
231+
const aliasMap = combineSetMaps(...Object.values(this.networkTargets), {
220232
// The Docker hostname always maps to the host's localhost, and it's not automatically included
221233
// on platforms (Windows & Mac) where Docker resolves it implicitly.
222-
[DOCKER_HOST_HOSTNAME]: new Set(['127.0.0.1'])
234+
'host.docker.internal': HostGatewaySet
235+
});
236+
237+
return _.mapValues(aliasMap, (targets): ReadonlySet<string> => {
238+
if ([...targets].some(t => t === HostGateway)) {
239+
// For all host-gateway targets, we simplify to direct traffic
240+
// directly back to the host itself:
241+
return new Set(['127.0.0.1']);
242+
} else {
243+
return targets as ReadonlySet<string>;
244+
}
223245
});
224246
}
225247

@@ -281,7 +303,9 @@ class DockerNetworkMonitor {
281303
return isInterceptedContainer(container, this.proxyPort);
282304
}
283305

284-
private async getNetworkAliases(networkId: string): Promise<{ [host: string]: ReadonlySet<string> } | undefined> {
306+
private async getNetworkAliases(networkId: string): Promise<
307+
{ [host: string]: ReadonlySet<string | HostGateway> } | undefined
308+
> {
285309
const networkDetails: Docker.NetworkInspectInfo = await this.docker.getNetwork(networkId).inspect();
286310
const isDefaultBridge = networkDetails.Options?.['com.docker.network.bridge.default_bridge'] === 'true';
287311

@@ -304,7 +328,14 @@ class DockerNetworkMonitor {
304328
return undefined;
305329
}
306330

307-
const aliases: Array<readonly [alias: string, targetIp: string]> = [];
331+
const aliases: Array<readonly [alias: string, targetIp: string | HostGateway]> = [];
332+
333+
aliases.push(['host.docker.internal', HostGateway]);
334+
aliases.push(['gateway.docker.internal', HostGateway]); // Seems equivalent? Very rarely used AFAICT.
335+
// Deprecated but still functional Docker Desktop aliases:
336+
if (process.platform === 'darwin') aliases.push(['docker.for.mac.localhost', HostGateway]);
337+
if (process.platform === 'win32') aliases.push(['docker.for.win.localhost', HostGateway]);
338+
308339

309340
/*
310341
* So, what names are resolveable on a network?
@@ -370,7 +401,7 @@ class DockerNetworkMonitor {
370401
const alias = hostParts[0];
371402
const target = hostParts.slice(1).join(':');
372403
const targetIp = target === 'host-gateway'
373-
? '127.0.0.1'
404+
? HostGateway
374405
: target;
375406
return [alias, targetIp] as const
376407
})
@@ -417,6 +448,6 @@ class DockerNetworkMonitor {
417448
if (!aliasMap[alias]) aliasMap[alias] = new Set();
418449
aliasMap[alias].add(target);
419450
return aliasMap;
420-
}, {} as { [alias: string]: Set<string> });
451+
}, {} as { [alias: string]: Set<string | HostGateway> });
421452
}
422453
}

test/fixtures/docker/compose/docker-compose.networks.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,20 @@ services:
2828
links:
2929
- "default-service-a:a"
3030

31+
extra-host-service:
32+
build: .
33+
extra_hosts:
34+
- 'custom.host.address.example:host-gateway'
35+
environment:
36+
EXTRA_TARGET: 'http://custom.host.address.example:9876' # The host container
37+
3138
multi-network-a:
3239
build: .
3340
networks:
3441
- custom_net_1
3542
- custom_net_2
43+
extra_hosts:
44+
- 'host.docker.internal:host-gateway'
3645
environment:
3746
EXTRA_TARGET: 'http://host.docker.internal:9876' # The host container
3847

test/fixtures/docker/compose/index.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,18 @@ const TARGETS = [
4040
// Can we remotely resolve our own loopback address?
4141
[`http://localhost:${SERVER_PORT}/`, isOurHostname],
4242
[`http://127.0.0.1:${SERVER_PORT}/`, isOurHostname],
43+
// (This works because Mockttp replaces localhost addresses in requests with
44+
// the client's IP)
45+
4346
// We can remote resolve our Docker hostname?
4447
[`http://${OUR_HOSTNAME}:${SERVER_PORT}/`, isOurHostname],
45-
// Can we resolve a mocked-only URL? (This will always fail normally)
48+
// (This works because our hostname is picked up by the network monitor, so the
49+
// request is sent via the tunnel, and our DNS server routes it to our IP.
50+
51+
// Can we resolve a mocked-only URL?
4652
[`https://example.test/`, is('Mock response')],
53+
// (This will always fail normally, but works in testing because we specifically
54+
// spot this and inject the response).
4755
];
4856

4957
if (process.env.EXTRA_TARGET) {
@@ -84,10 +92,10 @@ const pollInterval = setInterval(async () => {
8492
console.log("All requests ok");
8593
clearInterval(pollInterval);
8694

87-
// Exit OK, but after a delay, so the other container can still make requests to us.
95+
// Exit OK, but after a delay, so the other containers can still make requests to us.
8896
setTimeout(() => {
8997
process.exit(0);
90-
}, 2000);
98+
}, 5000);
9199
} else {
92100
console.log("Requests failed with", responses.map(r => r.message || r.statusCode));
93101
}

test/interceptors/docker-terminal-interception.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ Successfully built <hash>
303303
`host_1`,
304304
`default-service-a_1`,
305305
`default-linked-service-b_1`,
306+
`extra-host-service_1`,
306307
`multi-network-a_1`,
307308
`multi-network-b_1`
308309
].forEach((container) => {

0 commit comments

Comments
 (0)