localghost 3.2.0__tar.gz → 3.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {localghost-3.2.0 → localghost-3.2.1}/PKG-INFO +1 -1
- {localghost-3.2.0 → localghost-3.2.1}/pyproject.toml +1 -1
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/proxy_compose.yaml +5 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/proxy_compose_https.yaml +3 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/proxy_compose_tailscale.yaml +10 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/proxy_compose_tailscale_https.yaml +1 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/runner.py +7 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/tailscale_gateway/main.go +75 -13
- localghost-3.2.1/src/localghost/tailscale_gateway/proxy_test.go +145 -0
- localghost-3.2.1/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/.traefik.yml +7 -0
- localghost-3.2.1/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/client.go +143 -0
- localghost-3.2.1/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/client_test.go +118 -0
- localghost-3.2.1/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/go.mod +3 -0
- {localghost-3.2.0 → localghost-3.2.1}/LICENSE +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/README.md +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/.dockerignore +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/Dockerfile +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/__init__.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/cli.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/compose.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/config.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/feedback.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/forwarder.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/generator.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/paths.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/picker.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/ports.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/registry.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/roots.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/routes.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/sessions.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/statusbar.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/tailscale.py +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/tailscale_gateway/Dockerfile +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/tailscale_gateway/go.mod +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/tailscale_gateway/go.sum +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/tailscale_gateway/main_test.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/.traefik.yml +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/ca.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/ca_test.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/cmd/bootstrap/main.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/docker.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/docker_test.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/go.mod +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/go.sum +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/provider.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/provider_test.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/storage.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/LICENSE +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/config.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/http_config.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/marshaler.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/middlewares.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/plugins.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/tcp_config.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/tcp_middlewares.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/tls/certificate.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/tls/tls.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/types/domains.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/types/tls.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/udp_config.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/modules.txt +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/.traefik.yml +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/fallback.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/fallback_test.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/go.mod +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/logo.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-tailnet-ca/.traefik.yml +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-tailnet-ca/go.mod +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-tailnet-ca/provider.go +0 -0
- {localghost-3.2.0 → localghost-3.2.1}/src/localghost/trust.py +0 -0
|
@@ -19,6 +19,10 @@ services:
|
|
|
19
19
|
- --providers.docker.exposedbydefault=false
|
|
20
20
|
- --providers.docker.network=localghost
|
|
21
21
|
- --experimental.localplugins.localghostFallback.modulename=github.com/SmileyChris/traefik-localghost-fallback
|
|
22
|
+
# Docker hands Traefik loopback connections from an address of its own;
|
|
23
|
+
# every request passes this middleware so applications see 127.0.0.1.
|
|
24
|
+
- --experimental.localplugins.localghostClient.modulename=github.com/SmileyChris/traefik-localghost-client
|
|
25
|
+
- --entrypoints.web.http.middlewares=localghost-client@docker
|
|
22
26
|
healthcheck:
|
|
23
27
|
test: [CMD, traefik, healthcheck, --ping]
|
|
24
28
|
interval: 10s
|
|
@@ -49,6 +53,7 @@ services:
|
|
|
49
53
|
- traefik.http.routers.localghost-fallback.middlewares=localghost-fallback
|
|
50
54
|
- traefik.http.routers.localghost-fallback.service=noop@internal
|
|
51
55
|
- traefik.http.middlewares.localghost-fallback.plugin.localghostFallback.registryPath=/var/lib/localghost-registry
|
|
56
|
+
- traefik.http.middlewares.localghost-client.plugin.localghostClient.routesPath=/proc/net/route
|
|
52
57
|
|
|
53
58
|
networks:
|
|
54
59
|
localghost:
|
|
@@ -46,6 +46,9 @@ services:
|
|
|
46
46
|
- --experimental.abortonpluginfailure=true
|
|
47
47
|
- --experimental.localplugins.localghostCA.modulename=github.com/SmileyChris/traefik-localghost-ca
|
|
48
48
|
- --experimental.localplugins.localghostFallback.modulename=github.com/SmileyChris/traefik-localghost-fallback
|
|
49
|
+
- --experimental.localplugins.localghostClient.modulename=github.com/SmileyChris/traefik-localghost-client
|
|
50
|
+
- --entrypoints.web.http.middlewares=localghost-client@docker
|
|
51
|
+
- --entrypoints.websecure.http.middlewares=localghost-client@docker
|
|
49
52
|
ports:
|
|
50
53
|
- 127.0.0.1:${LOCALGHOST_HTTPS_PORT:-443}:443
|
|
51
54
|
volumes:
|
|
@@ -14,6 +14,16 @@ services:
|
|
|
14
14
|
- --api.insecure=false
|
|
15
15
|
- --entrypoints.web.address=:80
|
|
16
16
|
- --entrypoints.websecure.address=:443
|
|
17
|
+
# The gateway speaks for tailnet clients: it replaces their forwarded
|
|
18
|
+
# headers on plain HTTP and names the peer in a PROXY header on the TLS
|
|
19
|
+
# passthrough. It sits on a Docker network, and everything else that
|
|
20
|
+
# reaches these loopback-published entrypoints is this machine, arriving
|
|
21
|
+
# from the Docker gateway -- all private addresses.
|
|
22
|
+
- --entrypoints.web.forwardedheaders.trustedips=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7
|
|
23
|
+
- --entrypoints.websecure.proxyprotocol.trustedips=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7
|
|
24
|
+
- --entrypoints.web.http.middlewares=localghost-client@docker
|
|
25
|
+
- --entrypoints.websecure.http.middlewares=localghost-client@docker
|
|
26
|
+
- --experimental.localplugins.localghostClient.modulename=github.com/SmileyChris/traefik-localghost-client
|
|
17
27
|
- --global.checknewversion=false
|
|
18
28
|
- --global.sendanonymoususage=false
|
|
19
29
|
- --ping=true
|
|
@@ -21,6 +21,7 @@ services:
|
|
|
21
21
|
- --root-ca=/var/lib/localghost-root/rootCA.pem
|
|
22
22
|
- --http-target=traefik:80
|
|
23
23
|
- --https-target=traefik:443
|
|
24
|
+
- --https-proxy-protocol
|
|
24
25
|
volumes:
|
|
25
26
|
# The signer volume contains only the exported public root plus the
|
|
26
27
|
# constrained online signer. The offline root volume must never be
|
|
@@ -578,6 +578,13 @@ def create_run_bridge_compose(
|
|
|
578
578
|
":8080",
|
|
579
579
|
"--to",
|
|
580
580
|
f"http://host.docker.internal:{host_port}",
|
|
581
|
+
# Traefik is the edge that saw the client. Caddy would otherwise
|
|
582
|
+
# replace the client address and scheme with its own view of this
|
|
583
|
+
# internal hop, so hand Traefik's values on unchanged.
|
|
584
|
+
"--header-up",
|
|
585
|
+
"X-Forwarded-For: {http.request.header.X-Forwarded-For}",
|
|
586
|
+
"--header-up",
|
|
587
|
+
"X-Forwarded-Proto: {http.request.header.X-Forwarded-Proto}",
|
|
581
588
|
]
|
|
582
589
|
)
|
|
583
590
|
service["extra_hosts"] = CommentedSeq(["host.docker.internal:host-gateway"])
|
|
@@ -43,7 +43,10 @@ type configuration struct {
|
|
|
43
43
|
rootCA string
|
|
44
44
|
httpTarget string
|
|
45
45
|
httpsTarget string
|
|
46
|
-
|
|
46
|
+
// httpsProxyProtocol names each tailnet client to the HTTPS target in a
|
|
47
|
+
// PROXY protocol header; the hub's websecure entrypoint expects one.
|
|
48
|
+
httpsProxyProtocol bool
|
|
49
|
+
bootstrap bool
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
// servesHTTPS reports whether the gateway fronts a TLS-terminating Traefik.
|
|
@@ -62,6 +65,7 @@ func main() {
|
|
|
62
65
|
flag.StringVar(&cfg.rootCA, "root-ca", "", "public root certificate; empty serves HTTP only")
|
|
63
66
|
flag.StringVar(&cfg.httpTarget, "http-target", "traefik:80", "HTTP proxy target")
|
|
64
67
|
flag.StringVar(&cfg.httpsTarget, "https-target", "", "HTTPS TCP target; required with --root-ca")
|
|
68
|
+
flag.BoolVar(&cfg.httpsProxyProtocol, "https-proxy-protocol", false, "name each tailnet client to the HTTPS target in a PROXY protocol v1 header")
|
|
65
69
|
flag.BoolVar(&cfg.bootstrap, "bootstrap", false, "enroll from an auth key on stdin, then exit")
|
|
66
70
|
healthProbe := flag.Bool("health-probe", false, "check the running gateway's health listener, then exit")
|
|
67
71
|
flag.Parse()
|
|
@@ -164,7 +168,7 @@ func run(ctx context.Context, cfg configuration) error {
|
|
|
164
168
|
startDNS(ctx, server, cfg.suffix, ip4, ip6, errCh)
|
|
165
169
|
startHTTP(ctx, server, cfg, errCh)
|
|
166
170
|
if cfg.servesHTTPS() {
|
|
167
|
-
startTCPProxy(ctx, server, ":443", cfg.httpsTarget, errCh)
|
|
171
|
+
startTCPProxy(ctx, server, ":443", cfg.httpsTarget, cfg.httpsProxyProtocol, errCh)
|
|
168
172
|
}
|
|
169
173
|
startHealth(ctx, errCh)
|
|
170
174
|
|
|
@@ -330,20 +334,45 @@ func startHealth(ctx context.Context, errCh chan<- error) {
|
|
|
330
334
|
}()
|
|
331
335
|
}
|
|
332
336
|
|
|
337
|
+
// newHTTPProxy forwards tailnet HTTP to target with the original Host header.
|
|
338
|
+
// Traefik believes forwarded headers from the gateway, so whatever a tailnet
|
|
339
|
+
// client claimed about itself is dropped and replaced with the address its
|
|
340
|
+
// connection actually came from.
|
|
341
|
+
func newHTTPProxy(target string) *httputil.ReverseProxy {
|
|
342
|
+
upstream := &url.URL{Scheme: "http", Host: target}
|
|
343
|
+
return &httputil.ReverseProxy{
|
|
344
|
+
Rewrite: func(request *httputil.ProxyRequest) {
|
|
345
|
+
request.SetURL(upstream)
|
|
346
|
+
request.Out.Host = request.In.Host
|
|
347
|
+
stripForwardedHeaders(request.Out.Header)
|
|
348
|
+
request.SetXForwarded()
|
|
349
|
+
if client := request.Out.Header.Get("X-Forwarded-For"); client != "" {
|
|
350
|
+
request.Out.Header.Set("X-Real-Ip", client)
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// stripForwardedHeaders removes every header Traefik would take as a proxy's
|
|
357
|
+
// word about the original request: the X-Forwarded-* family, X-Real-Ip, and
|
|
358
|
+
// the standard Forwarded header.
|
|
359
|
+
func stripForwardedHeaders(header http.Header) {
|
|
360
|
+
for name := range header {
|
|
361
|
+
if strings.HasPrefix(name, "X-Forwarded-") {
|
|
362
|
+
header.Del(name)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
header.Del("X-Real-Ip")
|
|
366
|
+
header.Del("Forwarded")
|
|
367
|
+
}
|
|
368
|
+
|
|
333
369
|
func startHTTP(ctx context.Context, server *tsnet.Server, cfg configuration, errCh chan<- error) {
|
|
334
370
|
listener, err := server.Listen("tcp", ":80")
|
|
335
371
|
if err != nil {
|
|
336
372
|
errCh <- fmt.Errorf("listening for HTTP: %w", err)
|
|
337
373
|
return
|
|
338
374
|
}
|
|
339
|
-
|
|
340
|
-
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
341
|
-
originalDirector := proxy.Director
|
|
342
|
-
proxy.Director = func(request *http.Request) {
|
|
343
|
-
host := request.Host
|
|
344
|
-
originalDirector(request)
|
|
345
|
-
request.Host = host
|
|
346
|
-
}
|
|
375
|
+
proxy := newHTTPProxy(cfg.httpTarget)
|
|
347
376
|
handler := http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
|
348
377
|
if serveTrust(w, request, cfg) {
|
|
349
378
|
return
|
|
@@ -451,7 +480,7 @@ on the hosting machine.</p>
|
|
|
451
480
|
`, cfg.suffix, command, fingerprint)
|
|
452
481
|
}
|
|
453
482
|
|
|
454
|
-
func startTCPProxy(ctx context.Context, server *tsnet.Server, address, target string, errCh chan<- error) {
|
|
483
|
+
func startTCPProxy(ctx context.Context, server *tsnet.Server, address, target string, proxyProtocol bool, errCh chan<- error) {
|
|
455
484
|
listener, err := server.Listen("tcp", address)
|
|
456
485
|
if err != nil {
|
|
457
486
|
errCh <- fmt.Errorf("listening on %s: %w", address, err)
|
|
@@ -470,12 +499,16 @@ func startTCPProxy(ctx context.Context, server *tsnet.Server, address, target st
|
|
|
470
499
|
}
|
|
471
500
|
return
|
|
472
501
|
}
|
|
473
|
-
go proxyConnection(ctx, incoming, target)
|
|
502
|
+
go proxyConnection(ctx, incoming, target, proxyProtocol)
|
|
474
503
|
}
|
|
475
504
|
}()
|
|
476
505
|
}
|
|
477
506
|
|
|
478
|
-
|
|
507
|
+
// proxyConnection relays one tailnet connection to target. With proxyProtocol
|
|
508
|
+
// it first names the tailnet peer in a PROXY protocol v1 header: the stream is
|
|
509
|
+
// TLS the gateway never opens, so this is the only way Traefik can learn who
|
|
510
|
+
// connected rather than seeing the gateway itself.
|
|
511
|
+
func proxyConnection(ctx context.Context, incoming net.Conn, target string, proxyProtocol bool) {
|
|
479
512
|
defer incoming.Close()
|
|
480
513
|
dialer := net.Dialer{Timeout: 10 * time.Second}
|
|
481
514
|
outgoing, err := dialer.DialContext(ctx, "tcp", target)
|
|
@@ -484,6 +517,13 @@ func proxyConnection(ctx context.Context, incoming net.Conn, target string) {
|
|
|
484
517
|
return
|
|
485
518
|
}
|
|
486
519
|
defer outgoing.Close()
|
|
520
|
+
if proxyProtocol {
|
|
521
|
+
header := proxyProtocolHeader(incoming.RemoteAddr(), incoming.LocalAddr())
|
|
522
|
+
if _, err := io.WriteString(outgoing, header); err != nil {
|
|
523
|
+
log.Printf("proxy header to %s failed: %v", target, err)
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
}
|
|
487
527
|
var wait sync.WaitGroup
|
|
488
528
|
wait.Add(2)
|
|
489
529
|
copyHalf := func(destination, source net.Conn) {
|
|
@@ -497,3 +537,25 @@ func proxyConnection(ctx context.Context, incoming net.Conn, target string) {
|
|
|
497
537
|
go copyHalf(incoming, outgoing)
|
|
498
538
|
wait.Wait()
|
|
499
539
|
}
|
|
540
|
+
|
|
541
|
+
// proxyProtocolHeader renders the PROXY protocol v1 line for a connection from
|
|
542
|
+
// source to destination. v1 names one address family for both ends, so a pair
|
|
543
|
+
// that disagrees, or an address that is not ip:port, is sent as UNKNOWN and
|
|
544
|
+
// the receiver keeps the connection's own addresses.
|
|
545
|
+
func proxyProtocolHeader(source, destination net.Addr) string {
|
|
546
|
+
from, errFrom := netip.ParseAddrPort(source.String())
|
|
547
|
+
to, errTo := netip.ParseAddrPort(destination.String())
|
|
548
|
+
if errFrom != nil || errTo != nil {
|
|
549
|
+
return "PROXY UNKNOWN\r\n"
|
|
550
|
+
}
|
|
551
|
+
fromIP := from.Addr().Unmap().WithZone("")
|
|
552
|
+
toIP := to.Addr().Unmap().WithZone("")
|
|
553
|
+
if fromIP.Is4() != toIP.Is4() {
|
|
554
|
+
return "PROXY UNKNOWN\r\n"
|
|
555
|
+
}
|
|
556
|
+
family := "TCP4"
|
|
557
|
+
if fromIP.Is6() {
|
|
558
|
+
family = "TCP6"
|
|
559
|
+
}
|
|
560
|
+
return fmt.Sprintf("PROXY %s %s %s %d %d\r\n", family, fromIP, toIP, from.Port(), to.Port())
|
|
561
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"context"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"net"
|
|
9
|
+
"net/http"
|
|
10
|
+
"net/http/httptest"
|
|
11
|
+
"net/url"
|
|
12
|
+
"testing"
|
|
13
|
+
"time"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func TestHTTPProxyReplacesWhatTheClientClaimsAboutItself(t *testing.T) {
|
|
17
|
+
seen := make(chan *http.Request, 1)
|
|
18
|
+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
|
19
|
+
seen <- request
|
|
20
|
+
}))
|
|
21
|
+
defer backend.Close()
|
|
22
|
+
target, err := url.Parse(backend.URL)
|
|
23
|
+
if err != nil {
|
|
24
|
+
t.Fatal(err)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
request := httptest.NewRequest("GET", "http://demo.tail1234/", nil)
|
|
28
|
+
request.RemoteAddr = "100.101.102.103:4242"
|
|
29
|
+
request.Header.Set("X-Forwarded-For", "127.0.0.1")
|
|
30
|
+
request.Header.Set("X-Real-Ip", "127.0.0.1")
|
|
31
|
+
request.Header.Set("X-Forwarded-Port", "1")
|
|
32
|
+
request.Header.Set("Forwarded", "for=127.0.0.1")
|
|
33
|
+
newHTTPProxy(target.Host).ServeHTTP(httptest.NewRecorder(), request)
|
|
34
|
+
|
|
35
|
+
received := <-seen
|
|
36
|
+
if got := received.Header.Values("X-Forwarded-For"); len(got) != 1 || got[0] != "100.101.102.103" {
|
|
37
|
+
t.Fatalf("X-Forwarded-For = %q, want only the tailnet peer", got)
|
|
38
|
+
}
|
|
39
|
+
if got := received.Header.Get("X-Real-Ip"); got != "100.101.102.103" {
|
|
40
|
+
t.Fatalf("X-Real-Ip = %q, want the tailnet peer", got)
|
|
41
|
+
}
|
|
42
|
+
if got := received.Header.Get("X-Forwarded-Proto"); got != "http" {
|
|
43
|
+
t.Fatalf("X-Forwarded-Proto = %q, want http", got)
|
|
44
|
+
}
|
|
45
|
+
for _, name := range []string{"X-Forwarded-Port", "Forwarded"} {
|
|
46
|
+
if got := received.Header.Get(name); got != "" {
|
|
47
|
+
t.Fatalf("%s = %q, want the client's claim dropped", name, got)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if received.Host != "demo.tail1234" {
|
|
51
|
+
t.Fatalf("Host = %q, want the original host", received.Host)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func TestProxyProtocolHeaderNamesTheTailnetPeer(t *testing.T) {
|
|
56
|
+
tcp := func(ip string, port int) net.Addr {
|
|
57
|
+
return &net.TCPAddr{IP: net.ParseIP(ip), Port: port}
|
|
58
|
+
}
|
|
59
|
+
cases := []struct {
|
|
60
|
+
name string
|
|
61
|
+
source net.Addr
|
|
62
|
+
destination net.Addr
|
|
63
|
+
want string
|
|
64
|
+
}{
|
|
65
|
+
{"ipv4", tcp("100.101.102.103", 4242), tcp("100.64.0.1", 443), "PROXY TCP4 100.101.102.103 100.64.0.1 4242 443\r\n"},
|
|
66
|
+
{"ipv6", tcp("fd7a:115c:a1e0::1", 4242), tcp("fd7a:115c:a1e0::2", 443), "PROXY TCP6 fd7a:115c:a1e0::1 fd7a:115c:a1e0::2 4242 443\r\n"},
|
|
67
|
+
{"mixed families", tcp("fd7a:115c:a1e0::1", 4242), tcp("100.64.0.1", 443), "PROXY UNKNOWN\r\n"},
|
|
68
|
+
}
|
|
69
|
+
for _, tc := range cases {
|
|
70
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
71
|
+
if got := proxyProtocolHeader(tc.source, tc.destination); got != tc.want {
|
|
72
|
+
t.Fatalf("got %q, want %q", got, tc.want)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// relayOnce accepts one connection on a fresh listener, relays it to a fresh
|
|
79
|
+
// target, and returns the client end and the target's end of the stream.
|
|
80
|
+
func relayOnce(t *testing.T, proxyProtocol bool) (client, upstream net.Conn) {
|
|
81
|
+
t.Helper()
|
|
82
|
+
target, err := net.Listen("tcp", "127.0.0.1:0")
|
|
83
|
+
if err != nil {
|
|
84
|
+
t.Fatal(err)
|
|
85
|
+
}
|
|
86
|
+
t.Cleanup(func() { _ = target.Close() })
|
|
87
|
+
front, err := net.Listen("tcp", "127.0.0.1:0")
|
|
88
|
+
if err != nil {
|
|
89
|
+
t.Fatal(err)
|
|
90
|
+
}
|
|
91
|
+
t.Cleanup(func() { _ = front.Close() })
|
|
92
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
93
|
+
t.Cleanup(cancel)
|
|
94
|
+
go func() {
|
|
95
|
+
incoming, err := front.Accept()
|
|
96
|
+
if err != nil {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
proxyConnection(ctx, incoming, target.Addr().String(), proxyProtocol)
|
|
100
|
+
}()
|
|
101
|
+
client, err = net.Dial("tcp", front.Addr().String())
|
|
102
|
+
if err != nil {
|
|
103
|
+
t.Fatal(err)
|
|
104
|
+
}
|
|
105
|
+
t.Cleanup(func() { _ = client.Close() })
|
|
106
|
+
upstream, err = target.Accept()
|
|
107
|
+
if err != nil {
|
|
108
|
+
t.Fatal(err)
|
|
109
|
+
}
|
|
110
|
+
t.Cleanup(func() { _ = upstream.Close() })
|
|
111
|
+
_ = upstream.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
112
|
+
return client, upstream
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func TestProxyConnectionAnnouncesThePeerBeforeTheStream(t *testing.T) {
|
|
116
|
+
client, upstream := relayOnce(t, true)
|
|
117
|
+
if _, err := client.Write([]byte("hello")); err != nil {
|
|
118
|
+
t.Fatal(err)
|
|
119
|
+
}
|
|
120
|
+
reader := bufio.NewReader(upstream)
|
|
121
|
+
line, err := reader.ReadString('\n')
|
|
122
|
+
if err != nil {
|
|
123
|
+
t.Fatal(err)
|
|
124
|
+
}
|
|
125
|
+
want := fmt.Sprintf("PROXY TCP4 127.0.0.1 127.0.0.1 %d %d\r\n",
|
|
126
|
+
client.LocalAddr().(*net.TCPAddr).Port, client.RemoteAddr().(*net.TCPAddr).Port)
|
|
127
|
+
if line != want {
|
|
128
|
+
t.Fatalf("header = %q, want %q", line, want)
|
|
129
|
+
}
|
|
130
|
+
payload := make([]byte, 5)
|
|
131
|
+
if _, err := io.ReadFull(reader, payload); err != nil || string(payload) != "hello" {
|
|
132
|
+
t.Fatalf("payload = %q (%v), want the client's bytes untouched", payload, err)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func TestProxyConnectionWithoutProxyProtocolRelaysOnlyTheStream(t *testing.T) {
|
|
137
|
+
client, upstream := relayOnce(t, false)
|
|
138
|
+
if _, err := client.Write([]byte("hello")); err != nil {
|
|
139
|
+
t.Fatal(err)
|
|
140
|
+
}
|
|
141
|
+
payload := make([]byte, 5)
|
|
142
|
+
if _, err := io.ReadFull(upstream, payload); err != nil || string(payload) != "hello" {
|
|
143
|
+
t.Fatalf("payload = %q (%v), want the client's bytes first", payload, err)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Package traefik_localghost_client implements a Traefik middleware plugin
|
|
2
|
+
// that names requests from this machine as 127.0.0.1.
|
|
3
|
+
//
|
|
4
|
+
// The hub publishes its ports on loopback only, yet Docker hands Traefik those
|
|
5
|
+
// connections from an address of its own: the bridge gateway on a native
|
|
6
|
+
// daemon, the VM's gateway under Docker Desktop. Left alone, every
|
|
7
|
+
// application would be told that address instead of the loopback the browser
|
|
8
|
+
// used. Everything else that reaches Traefik is a container on one of its own
|
|
9
|
+
// networks (the tailnet gateway among them) or a tailnet device named by the
|
|
10
|
+
// gateway's PROXY header. So an IPv4 source that is the gateway, or is on
|
|
11
|
+
// none of Traefik's networks and is not a Tailscale address, is this machine.
|
|
12
|
+
package traefik_localghost_client
|
|
13
|
+
|
|
14
|
+
import (
|
|
15
|
+
"bufio"
|
|
16
|
+
"context"
|
|
17
|
+
"encoding/hex"
|
|
18
|
+
"errors"
|
|
19
|
+
"fmt"
|
|
20
|
+
"net"
|
|
21
|
+
"net/http"
|
|
22
|
+
"os"
|
|
23
|
+
"strings"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
type Config struct {
|
|
27
|
+
// RoutesPath is the kernel's IPv4 routing table inside the Traefik
|
|
28
|
+
// container: its default route names the host, and its directly
|
|
29
|
+
// connected routes name the networks containers arrive from.
|
|
30
|
+
RoutesPath string `json:"routesPath,omitempty"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func CreateConfig() *Config { return &Config{RoutesPath: "/proc/net/route"} }
|
|
34
|
+
|
|
35
|
+
// tailnet is Tailscale's IPv4 range. Tailnet devices are off every local
|
|
36
|
+
// network, but they are anything but this machine.
|
|
37
|
+
var tailnet = &net.IPNet{IP: net.IPv4(100, 64, 0, 0).To4(), Mask: net.CIDRMask(10, 32)}
|
|
38
|
+
|
|
39
|
+
type client struct {
|
|
40
|
+
next http.Handler
|
|
41
|
+
gateway net.IP
|
|
42
|
+
networks []*net.IPNet
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func New(_ context.Context, next http.Handler, config *Config, _ string) (http.Handler, error) {
|
|
46
|
+
if config == nil || config.RoutesPath == "" {
|
|
47
|
+
return nil, errors.New("localghost client: routesPath is required")
|
|
48
|
+
}
|
|
49
|
+
gateway, networks, err := readRoutes(config.RoutesPath)
|
|
50
|
+
if err != nil {
|
|
51
|
+
// Without its own networks to compare against, every source would
|
|
52
|
+
// look like this machine. Pass requests through untouched instead;
|
|
53
|
+
// applications then see Docker's address, as they would without us.
|
|
54
|
+
fmt.Fprintf(os.Stdout, "localghost client: leaving client addresses alone: %v\n", err)
|
|
55
|
+
return next, nil
|
|
56
|
+
}
|
|
57
|
+
return &client{next: next, gateway: gateway, networks: networks}, nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func (c *client) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
61
|
+
host, port, err := net.SplitHostPort(req.RemoteAddr)
|
|
62
|
+
if err == nil && c.isThisMachine(net.ParseIP(host)) {
|
|
63
|
+
req.RemoteAddr = net.JoinHostPort("127.0.0.1", port)
|
|
64
|
+
// Traefik has already recorded the connection's address here. A value
|
|
65
|
+
// it kept from a trusted sender is that sender's claim; leave it be.
|
|
66
|
+
if req.Header.Get("X-Real-Ip") == host {
|
|
67
|
+
req.Header.Set("X-Real-Ip", "127.0.0.1")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
c.next.ServeHTTP(rw, req)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func (c *client) isThisMachine(ip net.IP) bool {
|
|
74
|
+
// IPv6 is left alone: this table only lists IPv4 networks, so an IPv6
|
|
75
|
+
// container address would otherwise be mistaken for the host.
|
|
76
|
+
ip = ip.To4()
|
|
77
|
+
if ip == nil || ip.IsLoopback() {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
if ip.Equal(c.gateway) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
if tailnet.Contains(ip) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
for _, network := range c.networks {
|
|
87
|
+
if network.Contains(ip) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// readRoutes returns the default gateway and the directly connected networks
|
|
95
|
+
// from a /proc/net/route table.
|
|
96
|
+
func readRoutes(path string) (net.IP, []*net.IPNet, error) {
|
|
97
|
+
file, err := os.Open(path)
|
|
98
|
+
if err != nil {
|
|
99
|
+
return nil, nil, err
|
|
100
|
+
}
|
|
101
|
+
defer file.Close()
|
|
102
|
+
var gateway net.IP
|
|
103
|
+
var networks []*net.IPNet
|
|
104
|
+
scanner := bufio.NewScanner(file)
|
|
105
|
+
scanner.Scan() // header row
|
|
106
|
+
for scanner.Scan() {
|
|
107
|
+
fields := strings.Fields(scanner.Text())
|
|
108
|
+
if len(fields) < 8 {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
destination, errDestination := parseHexIPv4(fields[1])
|
|
112
|
+
via, errVia := parseHexIPv4(fields[2])
|
|
113
|
+
mask, errMask := parseHexIPv4(fields[7])
|
|
114
|
+
if errDestination != nil || errVia != nil || errMask != nil {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
switch {
|
|
118
|
+
case destination.Equal(net.IPv4zero) && !via.Equal(net.IPv4zero):
|
|
119
|
+
gateway = via
|
|
120
|
+
case via.Equal(net.IPv4zero):
|
|
121
|
+
networks = append(networks, &net.IPNet{IP: destination, Mask: net.IPMask(mask)})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if err := scanner.Err(); err != nil {
|
|
125
|
+
return nil, nil, err
|
|
126
|
+
}
|
|
127
|
+
if gateway == nil || len(networks) == 0 {
|
|
128
|
+
return nil, nil, fmt.Errorf("no default gateway and connected network in %s", path)
|
|
129
|
+
}
|
|
130
|
+
return gateway, networks, nil
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// parseHexIPv4 decodes a /proc/net/route address, which is little-endian hex.
|
|
134
|
+
func parseHexIPv4(field string) (net.IP, error) {
|
|
135
|
+
raw, err := hex.DecodeString(field)
|
|
136
|
+
if err != nil {
|
|
137
|
+
return nil, err
|
|
138
|
+
}
|
|
139
|
+
if len(raw) != 4 {
|
|
140
|
+
return nil, fmt.Errorf("not an IPv4 route field: %q", field)
|
|
141
|
+
}
|
|
142
|
+
return net.IPv4(raw[3], raw[2], raw[1], raw[0]).To4(), nil
|
|
143
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package traefik_localghost_client
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"net/http"
|
|
6
|
+
"net/http/httptest"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// A Traefik container on one network, 172.19.0.0/16, whose host is 172.19.0.1.
|
|
13
|
+
const routes = "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n" +
|
|
14
|
+
"eth0\t00000000\t010013AC\t0003\t0\t0\t0\t00000000\t0\t0\t0\n" +
|
|
15
|
+
"eth0\t000013AC\t00000000\t0001\t0\t0\t0\t0000FFFF\t0\t0\t0\n"
|
|
16
|
+
|
|
17
|
+
func writeRoutes(t *testing.T, content string) string {
|
|
18
|
+
t.Helper()
|
|
19
|
+
path := filepath.Join(t.TempDir(), "route")
|
|
20
|
+
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
21
|
+
t.Fatal(err)
|
|
22
|
+
}
|
|
23
|
+
return path
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// serve runs one request through the middleware and returns what the next
|
|
27
|
+
// handler saw as the client address and X-Real-Ip.
|
|
28
|
+
func serve(t *testing.T, routesPath, remote, realIP string) (string, string) {
|
|
29
|
+
t.Helper()
|
|
30
|
+
var seenRemote, seenRealIP string
|
|
31
|
+
next := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
|
|
32
|
+
seenRemote, seenRealIP = req.RemoteAddr, req.Header.Get("X-Real-Ip")
|
|
33
|
+
})
|
|
34
|
+
handler, err := New(context.Background(), next, &Config{RoutesPath: routesPath}, "localghost-client")
|
|
35
|
+
if err != nil {
|
|
36
|
+
t.Fatal(err)
|
|
37
|
+
}
|
|
38
|
+
req := httptest.NewRequest("GET", "http://demo.localhost/", nil)
|
|
39
|
+
req.RemoteAddr = remote
|
|
40
|
+
if realIP != "" {
|
|
41
|
+
req.Header.Set("X-Real-Ip", realIP)
|
|
42
|
+
}
|
|
43
|
+
handler.ServeHTTP(httptest.NewRecorder(), req)
|
|
44
|
+
return seenRemote, seenRealIP
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func TestRequestsFromThisMachineAreNamedLoopback(t *testing.T) {
|
|
48
|
+
path := writeRoutes(t, routes)
|
|
49
|
+
cases := []struct {
|
|
50
|
+
name, remote string
|
|
51
|
+
}{
|
|
52
|
+
{"native daemon's bridge gateway", "172.19.0.1:40226"},
|
|
53
|
+
{"Docker Desktop's VM gateway", "192.168.65.1:40226"},
|
|
54
|
+
}
|
|
55
|
+
for _, tc := range cases {
|
|
56
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
57
|
+
host := tc.remote[:len(tc.remote)-len(":40226")]
|
|
58
|
+
remote, realIP := serve(t, path, tc.remote, host)
|
|
59
|
+
if remote != "127.0.0.1:40226" || realIP != "127.0.0.1" {
|
|
60
|
+
t.Fatalf("got %s / %s, want 127.0.0.1:40226 / 127.0.0.1", remote, realIP)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func TestOtherClientsKeepTheirAddress(t *testing.T) {
|
|
67
|
+
path := writeRoutes(t, routes)
|
|
68
|
+
cases := []struct {
|
|
69
|
+
name, remote, realIP string
|
|
70
|
+
}{
|
|
71
|
+
{"a container on Traefik's network, such as the tailnet gateway", "172.19.0.3:5000", "172.19.0.3"},
|
|
72
|
+
{"a tailnet device named by a PROXY header", "100.101.102.103:5000", "100.101.102.103"},
|
|
73
|
+
{"an IPv6 peer", "[fd7a:115c:a1e0::1]:5000", "fd7a:115c:a1e0::1"},
|
|
74
|
+
{"loopback itself", "127.0.0.1:5000", "127.0.0.1"},
|
|
75
|
+
}
|
|
76
|
+
for _, tc := range cases {
|
|
77
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
78
|
+
remote, realIP := serve(t, path, tc.remote, tc.realIP)
|
|
79
|
+
if remote != tc.remote || realIP != tc.realIP {
|
|
80
|
+
t.Fatalf("got %s / %s, want %s / %s untouched", remote, realIP, tc.remote, tc.realIP)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func TestATrustedSendersRealIPIsNotOverwritten(t *testing.T) {
|
|
87
|
+
remote, realIP := serve(t, writeRoutes(t, routes), "172.19.0.1:40226", "10.9.9.9")
|
|
88
|
+
if remote != "127.0.0.1:40226" || realIP != "10.9.9.9" {
|
|
89
|
+
t.Fatalf("got %s / %s, want the connection renamed and the claim kept", remote, realIP)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func TestUnreadableRoutesLeaveEveryAddressAlone(t *testing.T) {
|
|
94
|
+
for name, path := range map[string]string{
|
|
95
|
+
"missing table": filepath.Join(t.TempDir(), "absent"),
|
|
96
|
+
"no default route": writeRoutes(t, "Iface\tDestination\tGateway\n"),
|
|
97
|
+
} {
|
|
98
|
+
t.Run(name, func(t *testing.T) {
|
|
99
|
+
remote, realIP := serve(t, path, "172.19.0.1:40226", "172.19.0.1")
|
|
100
|
+
if remote != "172.19.0.1:40226" || realIP != "172.19.0.1" {
|
|
101
|
+
t.Fatalf("got %s / %s, want the request passed through", remote, realIP)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func TestReadRoutesFindsTheHostAndTheLocalNetwork(t *testing.T) {
|
|
108
|
+
gateway, networks, err := readRoutes(writeRoutes(t, routes))
|
|
109
|
+
if err != nil {
|
|
110
|
+
t.Fatal(err)
|
|
111
|
+
}
|
|
112
|
+
if gateway.String() != "172.19.0.1" {
|
|
113
|
+
t.Fatalf("gateway = %s, want 172.19.0.1", gateway)
|
|
114
|
+
}
|
|
115
|
+
if len(networks) != 1 || networks[0].String() != "172.19.0.0/16" {
|
|
116
|
+
t.Fatalf("networks = %v, want [172.19.0.0/16]", networks)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|