feat: predefined docker image blacklist, avoid proxing service backends, refactor

This commit is contained in:
yusing 2025-03-28 08:05:23 +08:00
parent c6f65ba69f
commit 3c515b0258
6 changed files with 166 additions and 141 deletions

View file

@ -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,
}
)

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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)
})
}

View file

@ -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
}

View file

@ -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
}