Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type AllowListRegistry struct {

type AllowList struct {
ID string // Container ID (empty for the default allowlist)
ContainerName string // Container name (empty for the default allowlist)
AllowedRequests map[string][]*regexp.Regexp // map of request methods to request path regex patterns (no requests allowed if empty)
AllowedBindMounts []string // list of from portion of allowed bind mounts (all bind mounts allowed if empty)
}
Expand Down Expand Up @@ -341,7 +342,8 @@ func (cfg *Config) UpdateAllowLists() {
slog.Info("Docker event stream closed")
return
}
slog.Debug("received Docker container event", "action", event.Action, "id", event.Actor.ID[:12])
containerName := eventContainerName(event)
slog.Debug("received Docker container event", "action", event.Action, "container", containerName)
addedIPs, removedIPs, updateErr := cfg.AllowLists.updateFromEvent(ctx, dockerClient, event)
if updateErr != nil {
slog.Warn("failed to update allowlists from container event", "error", updateErr)
Expand All @@ -356,7 +358,7 @@ func (cfg *Config) UpdateAllowLists() {
}
}
for _, ip := range removedIPs {
slog.Info("removed allowlist for container", "id", event.Actor.ID[:12], "ip", ip)
slog.Info("removed allowlist for container", "container", containerName, "ip", ip)
}
case err := <-errChan:
if err != nil {
Expand Down Expand Up @@ -427,6 +429,7 @@ func (allowLists *AllowListRegistry) initByIP(ctx context.Context, dockerClient
if slices.Contains(allowLists.networks, networkID) {
allowList := AllowList{
ID: cntr.ID,
ContainerName: containerName(cntr),
AllowedRequests: allowedRequests,
AllowedBindMounts: allowedBindMounts,
}
Expand Down Expand Up @@ -483,7 +486,7 @@ func (allowLists *AllowListRegistry) add(
return nil, err
}
if len(containers) == 0 {
slog.Debug("container is not in a network with socket-proxy or may have stopped", "id", containerID[:12])
slog.Debug("container is not in a network with socket-proxy or may have stopped", "id", shortContainerID(containerID))
return nil, nil
}
cntr := containers[0]
Expand All @@ -497,6 +500,7 @@ func (allowLists *AllowListRegistry) add(
if len(allowedRequests) > 0 || len(allowedBindMounts) > 0 {
allowList := AllowList{
ID: cntr.ID,
ContainerName: containerName(cntr),
AllowedRequests: allowedRequests,
AllowedBindMounts: allowedBindMounts,
}
Expand Down Expand Up @@ -550,7 +554,7 @@ func (allowList AllowList) Print(ip string, logJSON bool) {
} else {
for method, regex := range allowList.AllowedRequests {
slog.Info("configured request allowlist",
"id", allowList.ID[:12],
"container", allowList.ContainerName,
"ip", ip,
"method", method,
"regex", regex,
Expand All @@ -563,7 +567,7 @@ func (allowList AllowList) Print(ip string, logJSON bool) {
if ip == "" {
fmt.Printf("Default request allowlist:\n %-8s %s\n", "Method", "Regex")
} else {
fmt.Printf("Request allowlist for %s (%s):\n %-8s %s\n", allowList.ID[:12], ip, "Method", "Regex")
fmt.Printf("Request allowlist for %s (%s):\n %-8s %s\n", allowList.ContainerName, ip, "Method", "Regex")
}
for method, regex := range allowList.AllowedRequests {
fmt.Printf(" %-8s %s\n", method, regex)
Expand All @@ -578,7 +582,7 @@ func (allowList AllowList) Print(ip string, logJSON bool) {
} else {
slog.Info("Docker bind mount restrictions enabled",
"allowbindmountfrom", allowList.AllowedBindMounts,
"id", allowList.ID[:12],
"container", allowList.ContainerName,
"ip", ip,
)
}
Expand All @@ -587,11 +591,38 @@ func (allowList AllowList) Print(ip string, logJSON bool) {
if ip == "" {
slog.Debug("no default Docker bind mount restrictions")
} else {
slog.Debug("no Docker bind mount restrictions", "id", allowList.ID[:12], "ip", ip)
slog.Debug("no Docker bind mount restrictions", "container", allowList.ContainerName, "ip", ip)
}
}
}

// containerName returns Docker's container name without its leading slash.
// It falls back to the short container ID for unusual responses without a name.
func containerName(cntr container.Summary) string {
for _, name := range cntr.Names {
if name = strings.TrimPrefix(name, "/"); name != "" {
return name
}
}
return shortContainerID(cntr.ID)
}

// eventContainerName returns the name Docker includes with container events.
// It falls back to the short container ID when the event has no name.
func eventContainerName(event events.Message) string {
if name := event.Actor.Attributes["name"]; name != "" {
return name
}
return shortContainerID(event.Actor.ID)
}

func shortContainerID(id string) string {
if len(id) > 12 {
return id[:12]
}
return id
}

// compile allowed requests regex pattern
func compileRegexp(regex, method, configLocation string) (*regexp.Regexp, error) {
r, err := regexp.Compile("^" + regex + "$")
Expand Down
60 changes: 60 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

"github.com/wollomatic/socket-proxy/internal/docker/api/types/container"
"github.com/wollomatic/socket-proxy/internal/docker/api/types/events"
)

func resetFlagsForTest(t *testing.T, args []string) func() {
Expand Down Expand Up @@ -128,6 +129,65 @@ func regexMapsEqual(a, b map[string][]*regexp.Regexp) bool {
return true
}

func TestContainerName(t *testing.T) {
tests := []struct {
name string
cntr container.Summary
want string
}{
{
name: "uses the first Docker container name",
cntr: container.Summary{ID: "0123456789abcdef", Names: []string{"/traefik", "/ignored"}},
want: "traefik",
},
{
name: "falls back to the short ID when Docker provides no name",
cntr: container.Summary{ID: "0123456789abcdef"},
want: "0123456789ab",
},
{
name: "does not panic for a short fallback ID",
cntr: container.Summary{ID: "short"},
want: "short",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := containerName(tt.cntr); got != tt.want {
t.Errorf("containerName() = %q, want %q", got, tt.want)
}
})
}
}

func TestEventContainerName(t *testing.T) {
tests := []struct {
name string
event events.Message
want string
}{
{
name: "uses the container name from event attributes",
event: events.Message{Actor: events.Actor{ID: "0123456789abcdef", Attributes: map[string]string{"name": "traefik"}}},
want: "traefik",
},
{
name: "falls back to the short ID when the event has no name",
event: events.Message{Actor: events.Actor{ID: "0123456789abcdef"}},
want: "0123456789ab",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := eventContainerName(tt.event); got != tt.want {
t.Errorf("eventContainerName() = %q, want %q", got, tt.want)
}
})
}
}

func TestInitConfig_AllowMethodFlagOverridesEnv(t *testing.T) {
t.Setenv("SP_ALLOW_GET", "/from-env")
restore := resetFlagsForTest(t, []string{"socket-proxy", "-allowGET=/from-flag"})
Expand Down