feat(docker): add Docker socket proxy support and related configurations
Some checks are pending
Docker Image CI (nightly) / build-nightly (push) Waiting to run
Docker Image CI (nightly) / build-nightly-agent (push) Waiting to run

- Introduced Docker socket proxy handling in the agent.
- Added environment variables for Docker socket configuration.
- Implemented new Docker handler with endpoint permissions based on environment settings.
- Added tests for Docker handler functionality.
- Updated go.mod to include gorilla/mux for routing.
This commit is contained in:
yusing 2025-05-08 20:59:32 +08:00
parent 8424fd9f1a
commit 455a85e6a0
9 changed files with 701 additions and 7 deletions

View file

@ -5,11 +5,13 @@ import (
"github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env" "github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/agent/pkg/handler"
"github.com/yusing/go-proxy/agent/pkg/server" "github.com/yusing/go-proxy/agent/pkg/server"
"github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/logging/memlogger" "github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/systeminfo" "github.com/yusing/go-proxy/internal/metrics/systeminfo"
httpServer "github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg" "github.com/yusing/go-proxy/pkg"
) )
@ -55,6 +57,17 @@ Tips:
} }
server.StartAgentServer(t, opts) server.StartAgentServer(t, opts)
if env.DockerSocketAddr != "" {
logging.Info().Msgf("Docker socket listening on: %s", env.DockerSocketAddr)
opts := httpServer.Options{
Name: "docker",
HTTPAddr: env.DockerSocketAddr,
Handler: handler.NewDockerHandler(),
}
httpServer.StartServer(t, opts)
}
systeminfo.Poller.Start() systeminfo.Poller.Start()
task.WaitExit(3) task.WaitExit(3)

View file

@ -7,6 +7,7 @@ replace github.com/yusing/go-proxy => ..
require ( require (
github.com/coder/websocket v1.8.13 github.com/coder/websocket v1.8.13
github.com/docker/docker v28.1.1+incompatible github.com/docker/docker v28.1.1+incompatible
github.com/gorilla/mux v1.8.1
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/yusing/go-proxy v0.12.3 github.com/yusing/go-proxy v0.12.3

View file

@ -84,6 +84,8 @@ github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I= github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=

View file

@ -9,6 +9,36 @@ services:
AGENT_PORT: "{{.Port}}" AGENT_PORT: "{{.Port}}"
AGENT_CA_CERT: "{{.CACert}}" AGENT_CA_CERT: "{{.CACert}}"
AGENT_SSL_CERT: "{{.SSLCert}}" AGENT_SSL_CERT: "{{.SSLCert}}"
# docker socket proxy
# unset DOCKER_SOCKET_ADDR to disable
DOCKER_SOCKET_ADDR: 127.0.0.1:2375
POST: false
ALLOW_RESTARTS: false
ALLOW_START: false
ALLOW_STOP: false
AUTH: false
BUILD: false
COMMIT: false
CONFIGS: false
CONTAINERS: false
DISTRIBUTIONS: false
EVENTS: true
EXEC: false
GRPC: false
IMAGES: false
INFO: false
NETWORKS: false
NODES: false
PING: true
PLUGINS: false
SECRETS: false
SERVICES: false
SESSION: false
SWARM: false
SYSTEM: false
TASKS: false
VERSION: true
VOLUMES: false
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data - ./data:/app/data

80
agent/pkg/env/env.go vendored
View file

@ -15,10 +15,82 @@ func DefaultAgentName() string {
} }
var ( var (
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName()) AgentName string
AgentPort = common.GetEnvInt("AGENT_PORT", 8890) AgentPort int
AgentSkipClientCertCheck bool
AgentCACert string
AgentSSLCert string
DockerSocketAddr string
DockerPost bool
DockerRestarts bool
DockerStart bool
DockerStop bool
DockerAuth bool
DockerBuild bool
DockerCommit bool
DockerConfigs bool
DockerContainers bool
DockerDistributions bool
DockerEvents bool
DockerExec bool
DockerGrpc bool
DockerImages bool
DockerInfo bool
DockerNetworks bool
DockerNodes bool
DockerPing bool
DockerPlugins bool
DockerSecrets bool
DockerServices bool
DockerSession bool
DockerSwarm bool
DockerSystem bool
DockerTasks bool
DockerVersion bool
DockerVolumes bool
)
func init() {
Load()
}
func Load() {
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
AgentPort = common.GetEnvInt("AGENT_PORT", 8890)
AgentSkipClientCertCheck = common.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false) AgentSkipClientCertCheck = common.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "") AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "") AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "")
)
// docker socket proxy
DockerSocketAddr = common.GetEnvString("DOCKER_SOCKET_ADDR", "127.0.0.1:2375")
DockerPost = common.GetEnvBool("POST", false)
DockerRestarts = common.GetEnvBool("ALLOW_RESTARTS", false)
DockerStart = common.GetEnvBool("ALLOW_START", false)
DockerStop = common.GetEnvBool("ALLOW_STOP", false)
DockerAuth = common.GetEnvBool("AUTH", false)
DockerBuild = common.GetEnvBool("BUILD", false)
DockerCommit = common.GetEnvBool("COMMIT", false)
DockerConfigs = common.GetEnvBool("CONFIGS", false)
DockerContainers = common.GetEnvBool("CONTAINERS", false)
DockerDistributions = common.GetEnvBool("DISTRIBUTIONS", false)
DockerEvents = common.GetEnvBool("EVENTS", true)
DockerExec = common.GetEnvBool("EXEC", false)
DockerGrpc = common.GetEnvBool("GRPC", false)
DockerImages = common.GetEnvBool("IMAGES", false)
DockerInfo = common.GetEnvBool("INFO", false)
DockerNetworks = common.GetEnvBool("NETWORKS", false)
DockerNodes = common.GetEnvBool("NODES", false)
DockerPing = common.GetEnvBool("PING", true)
DockerPlugins = common.GetEnvBool("PLUGINS", false)
DockerSecrets = common.GetEnvBool("SECRETS", false)
DockerServices = common.GetEnvBool("SERVICES", false)
DockerSession = common.GetEnvBool("SESSION", false)
DockerSwarm = common.GetEnvBool("SWARM", false)
DockerSystem = common.GetEnvBool("SYSTEM", false)
DockerTasks = common.GetEnvBool("TASKS", false)
DockerVersion = common.GetEnvBool("VERSION", true)
DockerVolumes = common.GetEnvBool("VOLUMES", false)
}

View file

@ -0,0 +1,414 @@
package handler
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/yusing/go-proxy/agent/pkg/env"
)
func TestNewDockerHandler(t *testing.T) {
tests := []struct {
name string
method string
path string
envSetup func()
wantStatusCode int
}{
{
name: "GET _ping allowed by default",
method: http.MethodGet,
path: "/_ping",
envSetup: func() {},
wantStatusCode: http.StatusOK,
},
{
name: "GET version allowed by default",
method: http.MethodGet,
path: "/version",
envSetup: func() {},
wantStatusCode: http.StatusOK,
},
{
name: "GET containers allowed when enabled",
method: http.MethodGet,
path: "/containers",
envSetup: func() {
env.DockerContainers = true
},
wantStatusCode: http.StatusOK,
},
{
name: "GET containers not allowed when disabled",
method: http.MethodGet,
path: "/containers",
envSetup: func() {
env.DockerContainers = false
},
wantStatusCode: http.StatusForbidden,
},
{
name: "POST not allowed by default",
method: http.MethodPost,
path: "/_ping",
envSetup: func() {
env.DockerPost = false
},
wantStatusCode: http.StatusMethodNotAllowed,
},
{
name: "POST allowed when enabled",
method: http.MethodPost,
path: "/_ping",
envSetup: func() {
env.DockerPost = true
env.DockerPing = true
},
wantStatusCode: http.StatusOK,
},
{
name: "Container restart not allowed when disabled",
method: http.MethodPost,
path: "/containers/test-container/restart",
envSetup: func() {
env.DockerPost = true
env.DockerContainers = true
env.DockerRestarts = false
},
wantStatusCode: http.StatusForbidden,
},
{
name: "Container restart allowed when enabled",
method: http.MethodPost,
path: "/containers/test-container/restart",
envSetup: func() {
env.DockerPost = true
env.DockerContainers = true
env.DockerRestarts = true
},
wantStatusCode: http.StatusOK,
},
{
name: "Container start not allowed when disabled",
method: http.MethodPost,
path: "/containers/test-container/start",
envSetup: func() {
env.DockerPost = true
env.DockerContainers = true
env.DockerStart = false
},
wantStatusCode: http.StatusForbidden,
},
{
name: "Container start allowed when enabled",
method: http.MethodPost,
path: "/containers/test-container/start",
envSetup: func() {
env.DockerPost = true
env.DockerContainers = true
env.DockerStart = true
},
wantStatusCode: http.StatusOK,
},
{
name: "Container stop not allowed when disabled",
method: http.MethodPost,
path: "/containers/test-container/stop",
envSetup: func() {
env.DockerPost = true
env.DockerContainers = true
env.DockerStop = false
},
wantStatusCode: http.StatusForbidden,
},
{
name: "Container stop allowed when enabled",
method: http.MethodPost,
path: "/containers/test-container/stop",
envSetup: func() {
env.DockerPost = true
env.DockerContainers = true
env.DockerStop = true
},
wantStatusCode: http.StatusOK,
},
{
name: "Versioned API paths work",
method: http.MethodGet,
path: "/v1.41/version",
envSetup: func() {
env.DockerVersion = true
},
wantStatusCode: http.StatusOK,
},
{
name: "PUT method not allowed",
method: http.MethodPut,
path: "/version",
envSetup: func() {
env.DockerVersion = true
},
wantStatusCode: http.StatusMethodNotAllowed,
},
{
name: "DELETE method not allowed",
method: http.MethodDelete,
path: "/version",
envSetup: func() {
env.DockerVersion = true
},
wantStatusCode: http.StatusMethodNotAllowed,
},
}
// Save original env values to restore after tests
originalContainers := env.DockerContainers
originalRestarts := env.DockerRestarts
originalStart := env.DockerStart
originalStop := env.DockerStop
originalPost := env.DockerPost
originalPing := env.DockerPing
originalVersion := env.DockerVersion
defer func() {
// Restore original values
env.DockerContainers = originalContainers
env.DockerRestarts = originalRestarts
env.DockerStart = originalStart
env.DockerStop = originalStop
env.DockerPost = originalPost
env.DockerPing = originalPing
env.DockerVersion = originalVersion
}()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup environment for this test
tt.envSetup()
// Create test handler that will record the response for verification
dockerHandler := NewDockerHandler()
// Test server to capture the response
recorder := httptest.NewRecorder()
// Create request
req, err := http.NewRequest(tt.method, tt.path, nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
// Process the request
dockerHandler.ServeHTTP(recorder, req)
// Check response
if recorder.Code != tt.wantStatusCode {
t.Errorf("Expected status code %d, got %d",
tt.wantStatusCode, recorder.Code)
}
})
}
}
// This test focuses on checking that all the path prefix handling works correctly
func TestNewDockerHandler_PathHandling(t *testing.T) {
tests := []struct {
name string
path string
envVarName string
envVarValue bool
method string
wantAllowed bool
}{
{"Container path", "/containers/json", "DockerContainers", true, http.MethodGet, true},
{"Container path disabled", "/containers/json", "DockerContainers", false, http.MethodGet, false},
{"Auth path", "/auth", "DockerAuth", true, http.MethodGet, true},
{"Auth path disabled", "/auth", "DockerAuth", false, http.MethodGet, false},
{"Build path", "/build", "DockerBuild", true, http.MethodGet, true},
{"Build path disabled", "/build", "DockerBuild", false, http.MethodGet, false},
{"Commit path", "/commit", "DockerCommit", true, http.MethodGet, true},
{"Commit path disabled", "/commit", "DockerCommit", false, http.MethodGet, false},
{"Configs path", "/configs", "DockerConfigs", true, http.MethodGet, true},
{"Configs path disabled", "/configs", "DockerConfigs", false, http.MethodGet, false},
{"Distributions path", "/distributions", "DockerDistributions", true, http.MethodGet, true},
{"Distributions path disabled", "/distributions", "DockerDistributions", false, http.MethodGet, false},
{"Events path", "/events", "DockerEvents", true, http.MethodGet, true},
{"Events path disabled", "/events", "DockerEvents", false, http.MethodGet, false},
{"Exec path", "/exec", "DockerExec", true, http.MethodGet, true},
{"Exec path disabled", "/exec", "DockerExec", false, http.MethodGet, false},
{"Grpc path", "/grpc", "DockerGrpc", true, http.MethodGet, true},
{"Grpc path disabled", "/grpc", "DockerGrpc", false, http.MethodGet, false},
{"Images path", "/images", "DockerImages", true, http.MethodGet, true},
{"Images path disabled", "/images", "DockerImages", false, http.MethodGet, false},
{"Info path", "/info", "DockerInfo", true, http.MethodGet, true},
{"Info path disabled", "/info", "DockerInfo", false, http.MethodGet, false},
{"Networks path", "/networks", "DockerNetworks", true, http.MethodGet, true},
{"Networks path disabled", "/networks", "DockerNetworks", false, http.MethodGet, false},
{"Nodes path", "/nodes", "DockerNodes", true, http.MethodGet, true},
{"Nodes path disabled", "/nodes", "DockerNodes", false, http.MethodGet, false},
{"Plugins path", "/plugins", "DockerPlugins", true, http.MethodGet, true},
{"Plugins path disabled", "/plugins", "DockerPlugins", false, http.MethodGet, false},
{"Secrets path", "/secrets", "DockerSecrets", true, http.MethodGet, true},
{"Secrets path disabled", "/secrets", "DockerSecrets", false, http.MethodGet, false},
{"Services path", "/services", "DockerServices", true, http.MethodGet, true},
{"Services path disabled", "/services", "DockerServices", false, http.MethodGet, false},
{"Session path", "/session", "DockerSession", true, http.MethodGet, true},
{"Session path disabled", "/session", "DockerSession", false, http.MethodGet, false},
{"Swarm path", "/swarm", "DockerSwarm", true, http.MethodGet, true},
{"Swarm path disabled", "/swarm", "DockerSwarm", false, http.MethodGet, false},
{"System path", "/system", "DockerSystem", true, http.MethodGet, true},
{"System path disabled", "/system", "DockerSystem", false, http.MethodGet, false},
{"Tasks path", "/tasks", "DockerTasks", true, http.MethodGet, true},
{"Tasks path disabled", "/tasks", "DockerTasks", false, http.MethodGet, false},
{"Volumes path", "/volumes", "DockerVolumes", true, http.MethodGet, true},
{"Volumes path disabled", "/volumes", "DockerVolumes", false, http.MethodGet, false},
// Test versioned paths
{"Versioned auth", "/v1.41/auth", "DockerAuth", true, http.MethodGet, true},
{"Versioned auth disabled", "/v1.41/auth", "DockerAuth", false, http.MethodGet, false},
}
defer func() {
// Restore original env values
env.Load()
}()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset all Docker* env vars to false for this test
env.Load()
// Enable POST for all these tests
env.DockerPost = true
// Set the specific env var for this test
switch tt.envVarName {
case "DockerContainers":
env.DockerContainers = tt.envVarValue
case "DockerRestarts":
env.DockerRestarts = tt.envVarValue
case "DockerStart":
env.DockerStart = tt.envVarValue
case "DockerStop":
env.DockerStop = tt.envVarValue
case "DockerAuth":
env.DockerAuth = tt.envVarValue
case "DockerBuild":
env.DockerBuild = tt.envVarValue
case "DockerCommit":
env.DockerCommit = tt.envVarValue
case "DockerConfigs":
env.DockerConfigs = tt.envVarValue
case "DockerDistributions":
env.DockerDistributions = tt.envVarValue
case "DockerEvents":
env.DockerEvents = tt.envVarValue
case "DockerExec":
env.DockerExec = tt.envVarValue
case "DockerGrpc":
env.DockerGrpc = tt.envVarValue
case "DockerImages":
env.DockerImages = tt.envVarValue
case "DockerInfo":
env.DockerInfo = tt.envVarValue
case "DockerNetworks":
env.DockerNetworks = tt.envVarValue
case "DockerNodes":
env.DockerNodes = tt.envVarValue
case "DockerPlugins":
env.DockerPlugins = tt.envVarValue
case "DockerSecrets":
env.DockerSecrets = tt.envVarValue
case "DockerServices":
env.DockerServices = tt.envVarValue
case "DockerSession":
env.DockerSession = tt.envVarValue
case "DockerSwarm":
env.DockerSwarm = tt.envVarValue
case "DockerSystem":
env.DockerSystem = tt.envVarValue
case "DockerTasks":
env.DockerTasks = tt.envVarValue
case "DockerVolumes":
env.DockerVolumes = tt.envVarValue
default:
t.Fatalf("Unknown env var: %s", tt.envVarName)
}
// Create test handler
dockerHandler := NewDockerHandler()
// Test server to capture the response
recorder := httptest.NewRecorder()
// Create request
req, err := http.NewRequest(tt.method, tt.path, nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
// Process the request
dockerHandler.ServeHTTP(recorder, req)
// Check if the status indicates if the path is allowed or not
isAllowed := recorder.Code != http.StatusForbidden
if isAllowed != tt.wantAllowed {
t.Errorf("Path %s with env %s=%v: got allowed=%v, want allowed=%v (status=%d)",
tt.path, tt.envVarName, tt.envVarValue, isAllowed, tt.wantAllowed, recorder.Code)
}
})
}
}
// TestNewDockerHandlerWithMockDocker mocks the Docker API to test the actual HTTP handler behavior
// This is a more comprehensive test that verifies the full request/response chain
func TestNewDockerHandlerWithMockDocker(t *testing.T) {
// Set up environment
env.DockerContainers = true
env.DockerPost = true
// Create the handler
handler := NewDockerHandler()
// Test a valid request
req, _ := http.NewRequest(http.MethodGet, "/containers", nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
if recorder.Code != http.StatusOK {
t.Errorf("Expected status OK for /containers, got %d", recorder.Code)
}
// Test a disallowed path
env.DockerContainers = false
handler = NewDockerHandler() // recreate with new env
req, _ = http.NewRequest(http.MethodGet, "/containers", nil)
recorder = httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
if recorder.Code != http.StatusForbidden {
t.Errorf("Expected status Forbidden for /containers when disabled, got %d", recorder.Code)
}
}

View file

@ -16,6 +16,13 @@ func serviceUnavailable(w http.ResponseWriter, r *http.Request) {
http.Error(w, "docker socket is not available", http.StatusServiceUnavailable) http.Error(w, "docker socket is not available", http.StatusServiceUnavailable)
} }
func mockDockerSocketHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("mock docker response"))
})
}
func DockerSocketHandler() http.HandlerFunc { func DockerSocketHandler() http.HandlerFunc {
dockerClient, err := docker.NewClient(common.DockerHostFromEnv) dockerClient, err := docker.NewClient(common.DockerHostFromEnv)
if err != nil { if err != nil {

View file

@ -4,9 +4,12 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"github.com/gorilla/mux"
"github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env" "github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging/memlogger" "github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/systeminfo" "github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
@ -47,3 +50,153 @@ func NewAgentHandler() http.Handler {
mux.ServeMux.HandleFunc("/", DockerSocketHandler()) mux.ServeMux.HandleFunc("/", DockerSocketHandler())
return mux return mux
} }
func endpointNotAllowed(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "Endpoint not allowed", http.StatusForbidden)
}
// ref: https://github.com/Tecnativa/docker-socket-proxy/blob/master/haproxy.cfg
func NewDockerHandler() http.Handler {
r := mux.NewRouter()
var socketHandler http.HandlerFunc
if common.IsTest {
socketHandler = mockDockerSocketHandler()
} else {
socketHandler = DockerSocketHandler()
}
const apiVersionPrefix = `/{version:(?:v[\d\.]+)?}`
const containerPath = "/containers/{id:[a-zA-Z0-9_.-]+}"
allowedPaths := []string{}
deniedPaths := []string{}
if env.DockerContainers {
allowedPaths = append(allowedPaths, "/containers")
if !env.DockerRestarts {
deniedPaths = append(deniedPaths, containerPath+"/stop")
deniedPaths = append(deniedPaths, containerPath+"/restart")
deniedPaths = append(deniedPaths, containerPath+"/kill")
}
if !env.DockerStart {
deniedPaths = append(deniedPaths, containerPath+"/start")
}
if !env.DockerStop && env.DockerRestarts {
deniedPaths = append(deniedPaths, containerPath+"/stop")
}
}
if env.DockerAuth {
allowedPaths = append(allowedPaths, "/auth")
}
if env.DockerBuild {
allowedPaths = append(allowedPaths, "/build")
}
if env.DockerCommit {
allowedPaths = append(allowedPaths, "/commit")
}
if env.DockerConfigs {
allowedPaths = append(allowedPaths, "/configs")
}
if env.DockerDistributions {
allowedPaths = append(allowedPaths, "/distributions")
}
if env.DockerEvents {
allowedPaths = append(allowedPaths, "/events")
}
if env.DockerExec {
allowedPaths = append(allowedPaths, "/exec")
}
if env.DockerGrpc {
allowedPaths = append(allowedPaths, "/grpc")
}
if env.DockerImages {
allowedPaths = append(allowedPaths, "/images")
}
if env.DockerInfo {
allowedPaths = append(allowedPaths, "/info")
}
if env.DockerNetworks {
allowedPaths = append(allowedPaths, "/networks")
}
if env.DockerNodes {
allowedPaths = append(allowedPaths, "/nodes")
}
if env.DockerPing {
allowedPaths = append(allowedPaths, "/_ping")
}
if env.DockerPlugins {
allowedPaths = append(allowedPaths, "/plugins")
}
if env.DockerSecrets {
allowedPaths = append(allowedPaths, "/secrets")
}
if env.DockerServices {
allowedPaths = append(allowedPaths, "/services")
}
if env.DockerSession {
allowedPaths = append(allowedPaths, "/session")
}
if env.DockerSwarm {
allowedPaths = append(allowedPaths, "/swarm")
}
if env.DockerSystem {
allowedPaths = append(allowedPaths, "/system")
}
if env.DockerTasks {
allowedPaths = append(allowedPaths, "/tasks")
}
if env.DockerVersion {
allowedPaths = append(allowedPaths, "/version")
}
if env.DockerVolumes {
allowedPaths = append(allowedPaths, "/volumes")
}
// Helper to determine if a path should be treated as a prefix
isPrefixPath := func(path string) bool {
return strings.Count(path, "/") == 1
}
// 1. Register Denied Paths (specific)
for _, path := range deniedPaths {
// Handle with version prefix
r.HandleFunc(apiVersionPrefix+path, endpointNotAllowed)
// Handle without version prefix
r.HandleFunc(path, endpointNotAllowed)
}
// 2. Register Allowed Paths
for _, p := range allowedPaths {
fullPathWithVersion := apiVersionPrefix + p
if isPrefixPath(p) {
r.PathPrefix(fullPathWithVersion).Handler(socketHandler)
r.PathPrefix(p).Handler(socketHandler)
} else {
r.HandleFunc(fullPathWithVersion, socketHandler)
r.HandleFunc(p, socketHandler)
}
}
// 3. Add fallback for any other routes
r.PathPrefix("/").HandlerFunc(endpointNotAllowed)
// HTTP method filtering
if !env.DockerPost {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
r.ServeHTTP(w, req)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodPost, http.MethodGet:
r.ServeHTTP(w, req)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
}

View file

@ -82,10 +82,12 @@ func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T
return defaultValue return defaultValue
} }
func stringstring(s string) (string, error) {
return s, nil
}
func GetEnvString(key string, defaultValue string) string { func GetEnvString(key string, defaultValue string) string {
return GetEnv(key, defaultValue, func(s string) (string, error) { return GetEnv(key, defaultValue, stringstring)
return s, nil
})
} }
func GetEnvBool(key string, defaultValue bool) bool { func GetEnvBool(key string, defaultValue bool) bool {