Skip to content

Commit 42bc4e3

Browse files
authored
feat: resolve Docker container names in proxy ACL and logs (#32)
## Summary - Adds a `DockerResolver` that queries the Docker daemon via its Unix socket to map container IPs to container names/IDs, with a configurable TTL cache. - Integrates Docker-based identity resolution into the `Bypass` plugin so ACL rules can match full Docker container names (e.g. `docker-myapp-1`) instead of raw IPs. - Derives the log method field (`HTTP/SOCKS5`) from the gost service name instead of hardcoding `SOCKS5`. ## Test plan - `DockerResolver` unit tests cover IP => name resolution and cache TTL expiry - `Bypass` plugin tests cover Docker identity resolution taking priority over username fallback - Manual test: start a Docker container, confirm its name appears in proxy logs and ACL evaluation ```shell # Build and restart go build ./cmd/greyproxy pkill -f "greyproxy serve" # Start with Docker resolver enabled GREYPROXY_DOCKER_ENABLED=true \ GREYPROXY_DOCKER_SOCKET=/var/run/docker.sock \ ./greyproxy serve -C greyproxy.yml # One time setup docker network create proxy-test # Test Docker naming resolution docker run --rm --name my-test-app \ --network proxy-test \ --add-host=host.docker.internal:host-gateway \ curlimages/curl \ curl -x http://host.docker.internal:43051 https://example.com ``` <img width="1254" height="1146" alt="image" src="https://github.com/user-attachments/assets/ee4a7c12-412f-4e16-8412-6832f0cd6a1e" />
1 parent 0a916b5 commit 42bc4e3

File tree

10 files changed

+790
-13
lines changed

10 files changed

+790
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ release
1010
debian
1111
bin
1212
.vscode
13+
.idea
1314

1415
# Architecture specific extensions/prefixes
1516
*.[568vq]

cmd/greyproxy/docker_env_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
greyproxy "github.com/greyhavenhq/greyproxy/internal/greyproxy"
7+
)
8+
9+
func TestApplyDockerEnvOverrides(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
env map[string]string
13+
initial greyproxy.DockerConfig
14+
wantEnabled bool
15+
wantSocket string
16+
}{
17+
{
18+
name: "no env vars: config unchanged",
19+
env: map[string]string{},
20+
initial: greyproxy.DockerConfig{Enabled: false, Socket: "/var/run/docker.sock"},
21+
wantEnabled: false,
22+
wantSocket: "/var/run/docker.sock",
23+
},
24+
{
25+
name: "ENABLED=true overrides disabled config",
26+
env: map[string]string{"GREYPROXY_DOCKER_ENABLED": "true"},
27+
initial: greyproxy.DockerConfig{Enabled: false, Socket: ""},
28+
wantEnabled: true,
29+
wantSocket: "",
30+
},
31+
{
32+
name: "ENABLED=false overrides enabled config",
33+
env: map[string]string{"GREYPROXY_DOCKER_ENABLED": "false"},
34+
initial: greyproxy.DockerConfig{Enabled: true, Socket: "/var/run/docker.sock"},
35+
wantEnabled: false,
36+
wantSocket: "/var/run/docker.sock",
37+
},
38+
{
39+
name: "ENABLED unset leaves config value intact",
40+
env: map[string]string{},
41+
initial: greyproxy.DockerConfig{Enabled: true, Socket: ""},
42+
wantEnabled: true,
43+
wantSocket: "",
44+
},
45+
{
46+
name: "SOCKET overrides config socket path",
47+
env: map[string]string{"GREYPROXY_DOCKER_SOCKET": "/run/podman/podman.sock"},
48+
initial: greyproxy.DockerConfig{Enabled: false, Socket: "/var/run/docker.sock"},
49+
wantEnabled: false,
50+
wantSocket: "/run/podman/podman.sock",
51+
},
52+
{
53+
name: "SOCKET empty string does not override config",
54+
env: map[string]string{"GREYPROXY_DOCKER_SOCKET": ""},
55+
initial: greyproxy.DockerConfig{Enabled: false, Socket: "/var/run/docker.sock"},
56+
wantEnabled: false,
57+
wantSocket: "/var/run/docker.sock",
58+
},
59+
{
60+
name: "both ENABLED and SOCKET override config",
61+
env: map[string]string{
62+
"GREYPROXY_DOCKER_ENABLED": "true",
63+
"GREYPROXY_DOCKER_SOCKET": "/run/podman/podman.sock",
64+
},
65+
initial: greyproxy.DockerConfig{Enabled: false, Socket: "/var/run/docker.sock"},
66+
wantEnabled: true,
67+
wantSocket: "/run/podman/podman.sock",
68+
},
69+
{
70+
name: "ENABLED with unrecognized value leaves config unchanged",
71+
env: map[string]string{"GREYPROXY_DOCKER_ENABLED": "yes"},
72+
initial: greyproxy.DockerConfig{Enabled: true, Socket: ""},
73+
wantEnabled: true,
74+
wantSocket: "",
75+
},
76+
}
77+
78+
for _, tt := range tests {
79+
t.Run(tt.name, func(t *testing.T) {
80+
// Isolate env changes to this subtest.
81+
for k, v := range tt.env {
82+
t.Setenv(k, v)
83+
}
84+
85+
cfg := greyproxy.Config{Docker: tt.initial}
86+
applyDockerEnvOverrides(&cfg)
87+
88+
if cfg.Docker.Enabled != tt.wantEnabled {
89+
t.Errorf("Enabled: got %v, want %v", cfg.Docker.Enabled, tt.wantEnabled)
90+
}
91+
if cfg.Docker.Socket != tt.wantSocket {
92+
t.Errorf("Socket: got %q, want %q", cfg.Docker.Socket, tt.wantSocket)
93+
}
94+
})
95+
}
96+
}

cmd/greyproxy/program.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,9 @@ func (p *program) buildGreyproxyService() error {
374374
gaCfg.Resolver = "resolver-0"
375375
}
376376

377+
378+
applyDockerEnvOverrides(&gaCfg)
379+
377380
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@greyproxy"})
378381

379382
// Create shared state (this also opens the DB)
@@ -492,7 +495,7 @@ func (p *program) buildGreyproxyService() error {
492495
if port == 0 {
493496
port = 443
494497
}
495-
containerName, _ := greyproxy_plugins.ResolveIdentity(info.ContainerName)
498+
containerName, _ := greyproxy_plugins.ResolveIdentity(info.ContainerName, "")
496499
go func() {
497500
reqCT := info.RequestHeaders.Get("Content-Type")
498501
respCT := info.ResponseHeaders.Get("Content-Type")
@@ -553,7 +556,7 @@ func (p *program) buildGreyproxyService() error {
553556
if port == 0 {
554557
port = 443
555558
}
556-
containerName, _ := greyproxy_plugins.ResolveIdentity(info.ContainerName)
559+
containerName, _ := greyproxy_plugins.ResolveIdentity(info.ContainerName, "")
557560
go func() {
558561
if err := greyproxy.UpdateLatestLogMitmSkipReason(shared.DB, containerName, host, port, info.MitmSkipReason); err != nil {
559562
log.Warnf("failed to update MITM skip reason: %v", err)
@@ -571,7 +574,7 @@ func (p *program) buildGreyproxyService() error {
571574
if port == 0 {
572575
port = 443
573576
}
574-
containerName, _ := greyproxy_plugins.ResolveIdentity(info.ContainerName)
577+
containerName, _ := greyproxy_plugins.ResolveIdentity(info.ContainerName, "")
575578

576579
// Resolve hostname from cache
577580
resolvedHostname := shared.Cache.ResolveIP(host)
@@ -586,10 +589,25 @@ func (p *program) buildGreyproxyService() error {
586589
return nil
587590
})
588591

592+
// Initialize Docker resolver if configured.
593+
var dockerResolver greyproxy_plugins.ContainerResolver
594+
if gaCfg.Docker.Enabled {
595+
socketPath := gaCfg.Docker.Socket
596+
if socketPath == "" {
597+
socketPath = "/var/run/docker.sock"
598+
}
599+
cacheTTL := gaCfg.Docker.CacheTTL
600+
if cacheTTL == 0 {
601+
cacheTTL = 30 * time.Second
602+
}
603+
dockerResolver = greyproxy.NewDockerResolver(socketPath, cacheTTL)
604+
log.Infof("docker resolver enabled (socket=%s, cacheTTL=%s)", socketPath, cacheTTL)
605+
}
606+
589607
// Create and register gost plugins
590608
autherPlugin := greyproxy_plugins.NewAuther()
591609
admissionPlugin := greyproxy_plugins.NewAdmission()
592-
bypassPlugin := greyproxy_plugins.NewBypass(shared.DB, shared.Cache, shared.Bus, shared.Waiters, shared.ConnTracker)
610+
bypassPlugin := greyproxy_plugins.NewBypass(shared.DB, shared.Cache, shared.Bus, shared.Waiters, shared.ConnTracker, dockerResolver)
593611
resolverPlugin := greyproxy_plugins.NewResolver(shared.Cache)
594612

595613
registry.AutherRegistry().Register(gaCfg.Auther, autherPlugin)
@@ -741,3 +759,21 @@ func decompressBody(body []byte, encoding string) []byte {
741759
}
742760
return decoded
743761
}
762+
763+
// applyDockerEnvOverrides configures Docker resolution from environment variables.
764+
// Docker is disabled by default; use these env vars to opt in:
765+
//
766+
// - GREYPROXY_DOCKER_ENABLED=true → enable Docker resolution
767+
// - GREYPROXY_DOCKER_ENABLED=false → explicitly disable (default)
768+
// - GREYPROXY_DOCKER_SOCKET=<path> → socket path (default: /var/run/docker.sock)
769+
func applyDockerEnvOverrides(cfg *greyproxy.Config) {
770+
switch os.Getenv("GREYPROXY_DOCKER_ENABLED") {
771+
case "true":
772+
cfg.Docker.Enabled = true
773+
case "false":
774+
cfg.Docker.Enabled = false
775+
}
776+
if v := os.Getenv("GREYPROXY_DOCKER_SOCKET"); v != "" {
777+
cfg.Docker.Socket = v
778+
}
779+
}

greyproxy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ greyproxy:
2424
notifications:
2525
enabled: true
2626

27+
2728
services:
2829
# HTTP/HTTPS proxy on port 3128 (backward compatible with Squid)
2930
- name: http-proxy

internal/greyproxy/config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package greyproxy
22

3+
import "time"
4+
35
// Config holds configuration for the embedded proxy API service.
46
type Config struct {
57
Addr string `yaml:"addr" json:"addr"`
@@ -10,9 +12,24 @@ type Config struct {
1012
Bypass string `yaml:"bypass" json:"bypass"`
1113
Resolver string `yaml:"resolver" json:"resolver"`
1214
Notifications NotificationsConfig `yaml:"notifications" json:"notifications"`
15+
Docker DockerConfig `yaml:"docker" json:"docker"`
1316
}
1417

1518
// NotificationsConfig controls OS desktop notifications for pending requests.
1619
type NotificationsConfig struct {
1720
Enabled bool `yaml:"enabled" json:"enabled"`
1821
}
22+
23+
// DockerConfig enables optional Docker socket integration for resolving container
24+
// IP addresses to container names. When enabled, the bypass plugin uses the Docker
25+
// API to map source IPs to the actual container name, producing more meaningful
26+
// ACL rule matching (e.g. "docker-backend-1" instead of "unknown-172.17.0.2").
27+
type DockerConfig struct {
28+
// Enabled controls whether Docker socket resolution is active.
29+
Enabled bool `yaml:"enabled" json:"enabled"`
30+
// Socket is the path to the Docker/Podman socket. Defaults to /var/run/docker.sock.
31+
Socket string `yaml:"socket" json:"socket"`
32+
// CacheTTL controls how long resolved container names are cached.
33+
// Accepts Go duration strings (e.g. "30s", "1m"). Defaults to 30s.
34+
CacheTTL time.Duration `yaml:"cacheTTL" json:"cacheTTL"`
35+
}

0 commit comments

Comments
 (0)