mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
feat(docker): add Docker socket proxy support and related configurations
- 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:
parent
8424fd9f1a
commit
455a85e6a0
9 changed files with 701 additions and 7 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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=
|
||||||
|
|
|
@ -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
80
agent/pkg/env/env.go
vendored
|
@ -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)
|
||||||
|
}
|
||||||
|
|
414
agent/pkg/handler/docker_handler_test.go
Normal file
414
agent/pkg/handler/docker_handler_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Reference in a new issue