From 3c515b02581daabaa4557dd26bc19fab2d5c7ed4 Mon Sep 17 00:00:00 2001 From: yusing Date: Fri, 28 Mar 2025 08:05:23 +0800 Subject: [PATCH] feat: predefined docker image blacklist, avoid proxing service backends, refactor --- internal/common/ports.go | 75 ------------------ internal/docker/container.go | 113 ++++++++++++++++++++-------- internal/docker/container_helper.go | 52 ++++++------- internal/docker/container_test.go | 4 +- internal/docker/image_blacklist.go | 59 +++++++++++++++ internal/docker/inspect.go | 4 +- 6 files changed, 166 insertions(+), 141 deletions(-) delete mode 100644 internal/common/ports.go create mode 100644 internal/docker/image_blacklist.go diff --git a/internal/common/ports.go b/internal/common/ports.go deleted file mode 100644 index 7816355..0000000 --- a/internal/common/ports.go +++ /dev/null @@ -1,75 +0,0 @@ -package common - -var ( - WellKnownHTTPPorts = map[string]bool{ - "80": true, - "8000": true, - "8008": true, - "8080": true, - "3000": true, - } - - ServiceNamePortMapTCP = map[string]int{ - "mssql": 1433, - "mysql": 3306, - "mariadb": 3306, - "postgres": 5432, - "rabbitmq": 5672, - "redis": 6379, - "memcached": 11211, - "mongo": 27017, - "minecraft-server": 25565, - - "ssh": 22, - "ftp": 21, - "smtp": 25, - "dns": 53, - "pop3": 110, - "imap": 143, - } - - ImageNamePortMap = func() (m map[string]int) { - m = make(map[string]int, len(ServiceNamePortMapTCP)+len(imageNamePortMap)) - for k, v := range ServiceNamePortMapTCP { - m[k] = v - } - for k, v := range imageNamePortMap { - m[k] = v - } - return - }() - - imageNamePortMap = map[string]int{ - "adguardhome": 3000, - "bazarr": 6767, - "calibre-web": 8083, - "changedetection.io": 3000, - "dockge": 5001, - "gitea": 3000, - "gogs": 3000, - "grafana": 3000, - "home-assistant": 8123, - "homebridge": 8581, - "httpd": 80, - "immich": 3001, - "jellyfin": 8096, - "lidarr": 8686, - "microbin": 8080, - "nginx": 80, - "nginx-proxy-manager": 81, - "open-webui": 8080, - "plex": 32400, - "portainer-be": 9443, - "portainer-ce": 9443, - "prometheus": 9090, - "prowlarr": 9696, - "radarr": 7878, - "radarr-sma": 7878, - "rsshub": 1200, - "rss-bridge": 80, - "sonarr": 8989, - "sonarr-sma": 8989, - "uptime-kuma": 3001, - "whisparr": 6969, - } -) diff --git a/internal/docker/container.go b/internal/docker/container.go index b8e341f..728a918 100644 --- a/internal/docker/container.go +++ b/internal/docker/container.go @@ -5,34 +5,39 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + "github.com/yusing/go-proxy/agent/pkg/agent" + config "github.com/yusing/go-proxy/internal/config/types" "github.com/yusing/go-proxy/internal/logging" U "github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils/strutils" ) type ( - PortMapping = map[int]types.Port + PortMapping = map[int]container.Port Container struct { _ U.NoCopy - DockerHost string `json:"docker_host"` - ContainerName string `json:"container_name"` - ContainerID string `json:"container_id"` - ImageName string `json:"image_name"` + DockerHost string `json:"docker_host"` + Image *ContainerImage `json:"image"` + ContainerName string `json:"container_name"` + ContainerID string `json:"container_id"` + + Agent *agent.AgentConfig `json:"agent"` Labels map[string]string `json:"-"` + Mounts []string `json:"mounts"` + PublicPortMapping PortMapping `json:"public_ports"` // non-zero publicPort:types.Port PrivatePortMapping PortMapping `json:"private_ports"` // privatePort:types.Port - PublicIP string `json:"public_ip"` - PrivateIP string `json:"private_ip"` - NetworkMode string `json:"network_mode"` + PublicHostname string `json:"public_hostname"` + PrivateHostname string `json:"private_hostname"` Aliases []string `json:"aliases"` IsExcluded bool `json:"is_excluded"` IsExplicit bool `json:"is_explicit"` - IsDatabase bool `json:"is_database"` IdleTimeout string `json:"idle_timeout,omitempty"` WakeTimeout string `json:"wake_timeout,omitempty"` StopMethod string `json:"stop_method,omitempty"` @@ -41,35 +46,41 @@ type ( StartEndpoint string `json:"start_endpoint,omitempty"` Running bool `json:"running"` } + ContainerImage struct { + Author string `json:"author,omitempty"` + Name string `json:"name"` + Tag string `json:"tag,omitempty"` + } ) var DummyContainer = new(Container) -func FromDocker(c *types.Container, dockerHost string) (res *Container) { +func FromDocker(c *container.Summary, dockerHost string) (res *Container) { isExplicit := false helper := containerHelper{c} for lbl := range c.Labels { if strings.HasPrefix(lbl, NSProxy+".") { isExplicit = true - break + } else { + delete(c.Labels, lbl) } } res = &Container{ DockerHost: dockerHost, + Image: helper.parseImage(), ContainerName: helper.getName(), ContainerID: c.ID, - ImageName: helper.getImageName(), Labels: c.Labels, + Mounts: helper.getMounts(), + PublicPortMapping: helper.getPublicPortMapping(), PrivatePortMapping: helper.getPrivatePortMapping(), - NetworkMode: c.HostConfig.NetworkMode, Aliases: helper.getAliases(), IsExcluded: strutils.ParseBool(helper.getDeleteLabel(LabelExclude)), IsExplicit: isExplicit, - IsDatabase: helper.isDatabase(), IdleTimeout: helper.getDeleteLabel(LabelIdleTimeout), WakeTimeout: helper.getDeleteLabel(LabelWakeTimeout), StopMethod: helper.getDeleteLabel(LabelStopMethod), @@ -78,23 +89,32 @@ func FromDocker(c *types.Container, dockerHost string) (res *Container) { StartEndpoint: helper.getDeleteLabel(LabelStartEndpoint), Running: c.Status == "running" || c.State == "running", } - res.setPrivateIP(helper) - res.setPublicIP() + + if agent.IsDockerHostAgent(dockerHost) { + var ok bool + res.Agent, ok = config.GetInstance().GetAgent(dockerHost) + if !ok { + logging.Error().Msgf("agent %q not found", dockerHost) + } + } + + res.setPrivateHostname(helper) + res.setPublicHostname() return } -func FromJSON(json types.ContainerJSON, dockerHost string) *Container { - ports := make([]types.Port, 0) +func FromInspectResponse(json container.InspectResponse, dockerHost string) *Container { + ports := make([]container.Port, 0) for k, bindings := range json.NetworkSettings.Ports { - privPortStr, proto := k.Port(), k.Proto() + proto, privPortStr := nat.SplitProtoPort(string(k)) privPort, _ := strconv.ParseUint(privPortStr, 10, 16) - ports = append(ports, types.Port{ + ports = append(ports, container.Port{ PrivatePort: uint16(privPort), Type: proto, }) for _, v := range bindings { pubPort, _ := strconv.ParseUint(v.HostPort, 10, 16) - ports = append(ports, types.Port{ + ports = append(ports, container.Port{ IP: v.HostIP, PublicPort: uint16(pubPort), PrivatePort: uint16(privPort), @@ -102,7 +122,7 @@ func FromJSON(json types.ContainerJSON, dockerHost string) *Container { }) } } - cont := FromDocker(&types.Container{ + cont := FromDocker(&container.Summary{ ID: json.ID, Names: []string{strings.TrimPrefix(json.Name, "/")}, Image: json.Image, @@ -111,33 +131,62 @@ func FromJSON(json types.ContainerJSON, dockerHost string) *Container { State: json.State.Status, Status: json.State.Status, Mounts: json.Mounts, - NetworkSettings: &types.SummaryNetworkSettings{ + NetworkSettings: &container.NetworkSettingsSummary{ Networks: json.NetworkSettings.Networks, }, }, dockerHost) - cont.NetworkMode = string(json.HostConfig.NetworkMode) return cont } -func (c *Container) setPublicIP() { +func (c *Container) IsBlacklisted() bool { + return c.Image.IsBlacklisted() || c.isDatabase() +} + +var databaseMPs = map[string]struct{}{ + "/var/lib/postgresql/data": {}, + "/var/lib/mysql": {}, + "/var/lib/mongodb": {}, + "/var/lib/mariadb": {}, + "/var/lib/memcached": {}, + "/var/lib/rabbitmq": {}, +} + +func (c *Container) isDatabase() bool { + for _, m := range c.Mounts { + if _, ok := databaseMPs[m]; ok { + return true + } + } + + for _, v := range c.PrivatePortMapping { + switch v.PrivatePort { + // postgres, mysql or mariadb, redis, memcached, mongodb + case 5432, 3306, 6379, 11211, 27017: + return true + } + } + return false +} + +func (c *Container) setPublicHostname() { if !c.Running { return } if strings.HasPrefix(c.DockerHost, "unix://") { - c.PublicIP = "127.0.0.1" + c.PublicHostname = "127.0.0.1" return } url, err := url.Parse(c.DockerHost) if err != nil { logging.Err(err).Msgf("invalid docker host %q, falling back to 127.0.0.1", c.DockerHost) - c.PublicIP = "127.0.0.1" + c.PublicHostname = "127.0.0.1" return } - c.PublicIP = url.Hostname() + c.PublicHostname = url.Hostname() } -func (c *Container) setPrivateIP(helper containerHelper) { - if !strings.HasPrefix(c.DockerHost, "unix://") { +func (c *Container) setPrivateHostname(helper containerHelper) { + if !strings.HasPrefix(c.DockerHost, "unix://") && c.Agent == nil { return } if helper.NetworkSettings == nil { @@ -147,7 +196,7 @@ func (c *Container) setPrivateIP(helper containerHelper) { if v.IPAddress == "" { continue } - c.PrivateIP = v.IPAddress + c.PrivateHostname = v.IPAddress return } } diff --git a/internal/docker/container_helper.go b/internal/docker/container_helper.go index 0d6fbd6..59444c0 100644 --- a/internal/docker/container_helper.go +++ b/internal/docker/container_helper.go @@ -3,12 +3,12 @@ package docker import ( "strings" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/yusing/go-proxy/internal/utils/strutils" ) type containerHelper struct { - *types.Container + *container.Summary } // getDeleteLabel gets the value of a label and then deletes it from the container. @@ -32,10 +32,28 @@ func (c containerHelper) getName() string { return strings.TrimPrefix(c.Names[0], "/") } -func (c containerHelper) getImageName() string { +func (c containerHelper) getMounts() []string { + m := make([]string, len(c.Mounts)) + for i, v := range c.Mounts { + m[i] = v.Destination + } + return m +} + +func (c containerHelper) parseImage() *ContainerImage { colonSep := strutils.SplitRune(c.Image, ':') slashSep := strutils.SplitRune(colonSep[0], '/') - return slashSep[len(slashSep)-1] + im := new(ContainerImage) + if len(slashSep) > 1 { + im.Author = strings.Join(slashSep[:len(slashSep)-1], "/") + im.Name = slashSep[len(slashSep)-1] + } else { + im.Name = slashSep[0] + } + if len(colonSep) > 1 { + im.Tag = colonSep[1] + } + return im } func (c containerHelper) getPublicPortMapping() PortMapping { @@ -56,29 +74,3 @@ func (c containerHelper) getPrivatePortMapping() PortMapping { } return res } - -var databaseMPs = map[string]struct{}{ - "/var/lib/postgresql/data": {}, - "/var/lib/mysql": {}, - "/var/lib/mongodb": {}, - "/var/lib/mariadb": {}, - "/var/lib/memcached": {}, - "/var/lib/rabbitmq": {}, -} - -func (c containerHelper) isDatabase() bool { - for _, m := range c.Mounts { - if _, ok := databaseMPs[m.Destination]; ok { - return true - } - } - - for _, v := range c.Ports { - switch v.PrivatePort { - // postgres, mysql or mariadb, redis, memcached, mongodb - case 5432, 3306, 6379, 11211, 27017: - return true - } - } - return false -} diff --git a/internal/docker/container_test.go b/internal/docker/container_test.go index 4ecd475..753ebac 100644 --- a/internal/docker/container_test.go +++ b/internal/docker/container_test.go @@ -3,7 +3,7 @@ package docker import ( "testing" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" . "github.com/yusing/go-proxy/internal/utils/testing" ) @@ -36,7 +36,7 @@ func TestContainerExplicit(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := FromDocker(&types.Container{Names: []string{"test"}, State: "test", Labels: tt.labels}, "") + c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, "") ExpectEqual(t, c.IsExplicit, tt.isExplicit) }) } diff --git a/internal/docker/image_blacklist.go b/internal/docker/image_blacklist.go new file mode 100644 index 0000000..a7a7c3b --- /dev/null +++ b/internal/docker/image_blacklist.go @@ -0,0 +1,59 @@ +package docker + +var imageBlacklist = map[string]struct{}{ + // pure databases without UI + "postgres": {}, + "mysql": {}, + "mariadb": {}, + "redis": {}, + "memcached": {}, + "mongo": {}, + "rabbitmq": {}, + "couchdb": {}, + "neo4j": {}, + "telegraf": {}, + + // search engines, usually used for internal services + "elasticsearch": {}, + "meilisearch": {}, + "kibana": {}, + "solr": {}, +} + +var imageBlacklistFullname = map[string]struct{}{ + // headless browsers + "gcr.io/zenika-hub/alpine-chrome": {}, + "eu.gcr.io/zenika-hub/alpine-chrome": {}, + "us.gcr.io/zenika-hub/alpine-chrome": {}, + "asia.gcr.io/zenika-hub/alpine-chrome": {}, + + // image update watchers + "watchtower": {}, + "getwud/wud": {}, +} + +var authorBlacklist = map[string]struct{}{ + // headless browsers + "selenium": {}, + "browserless": {}, + "zenika": {}, + + "zabbix": {}, + + // docker + "moby": {}, + "docker": {}, +} + +func (image *ContainerImage) IsBlacklisted() bool { + _, ok := imageBlacklist[image.Name] + if ok { + return true + } + _, ok = imageBlacklistFullname[image.Author+":"+image.Name] + if ok { + return true + } + _, ok = authorBlacklist[image.Author] + return ok +} diff --git a/internal/docker/inspect.go b/internal/docker/inspect.go index 3932837..8eb1413 100644 --- a/internal/docker/inspect.go +++ b/internal/docker/inspect.go @@ -7,7 +7,7 @@ import ( ) func Inspect(dockerHost string, containerID string) (*Container, error) { - client, err := ConnectClient(dockerHost) + client, err := NewClient(dockerHost) if err != nil { return nil, err } @@ -24,5 +24,5 @@ func (c *SharedClient) Inspect(containerID string) (*Container, error) { if err != nil { return nil, err } - return FromJSON(json, c.key), nil + return FromInspectResponse(json, c.key), nil }