Skip to content

Commit 3e8675c

Browse files
authored
Merge branch 'develop' into dependabot/docker/golang-1.25.5-alpine3.22
Signed-off-by: Wolfgang Ellsässer <67168186+wollomatic@users.noreply.github.com>
2 parents 6ea3d8f + eead766 commit 3e8675c

26 files changed

Lines changed: 2018 additions & 112 deletions

LICENSE

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
SOFTWARE.
2222

2323
---
24-
Parts of this project, specifically the file cmd/internal/bindmount.go,
24+
Parts of this project, specifically the file cmd/socket-proxy/bindmount.go and
25+
the files in the internal/docker and internal/go-connections folders,
2526
contain source code licensed under the Apache License 2.0. See the comments
26-
in that file for details.
27+
in the applicable files for details.
2728
The rest of the project is licensed under the MIT License.
2829

2930
Apache License

README.md

Lines changed: 44 additions & 17 deletions
Large diffs are not rendered by default.

cmd/socket-proxy/bindmount.go

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ type (
7979
)
8080

8181
// checkBindMountRestrictions checks if bind mounts in the request are allowed.
82-
func checkBindMountRestrictions(r *http.Request) error {
82+
func checkBindMountRestrictions(allowedBindMounts []string, r *http.Request) error {
8383
// Only check if bind mount restrictions are configured
84-
if len(cfg.AllowBindMountFrom) == 0 {
84+
if len(allowedBindMounts) == 0 {
8585
return nil
8686
}
8787

@@ -94,23 +94,23 @@ func checkBindMountRestrictions(r *http.Request) error {
9494
switch {
9595
case len(pathParts) >= 4 && pathParts[2] == "containers" && pathParts[3] == "create":
9696
// Container creation: /vX.xx/containers/create
97-
return checkContainer(r)
97+
return checkContainer(allowedBindMounts, r)
9898
case len(pathParts) >= 5 && pathParts[2] == "containers" && pathParts[4] == "update":
9999
// Container update: /vX.xx/containers/{id}/update
100-
return checkContainer(r)
100+
return checkContainer(allowedBindMounts, r)
101101
case len(pathParts) >= 4 && pathParts[2] == "services" && pathParts[3] == "create":
102102
// Service creation: /vX.xx/services/create
103-
return checkService(r)
103+
return checkService(allowedBindMounts, r)
104104
case len(pathParts) >= 5 && pathParts[2] == "services" && pathParts[4] == "update":
105105
// Service update: /vX.xx/services/{id}/update
106-
return checkService(r)
106+
return checkService(allowedBindMounts, r)
107107
default:
108108
return nil
109109
}
110110
}
111111

112112
// checkContainer checks bind mounts in container creation requests.
113-
func checkContainer(r *http.Request) error {
113+
func checkContainer(allowedBindMounts []string, r *http.Request) error {
114114
body, err := readAndRestoreBody(r)
115115
if err != nil {
116116
return err
@@ -122,11 +122,11 @@ func checkContainer(r *http.Request) error {
122122
return nil // Don't block if we can't parse.
123123
}
124124

125-
return checkHostConfigBindMounts(req.HostConfig)
125+
return checkHostConfigBindMounts(allowedBindMounts, req.HostConfig)
126126
}
127127

128128
// checkService checks bind mounts in service creation requests.
129-
func checkService(r *http.Request) error {
129+
func checkService(allowedBindMounts []string, r *http.Request) error {
130130
body, err := readAndRestoreBody(r)
131131
if err != nil {
132132
return err
@@ -141,28 +141,31 @@ func checkService(r *http.Request) error {
141141
if req.TaskTemplate.ContainerSpec == nil {
142142
return nil // No container spec, nothing to check.
143143
}
144-
return checkHostConfigBindMounts(&containerHostConfig{
145-
Mounts: req.TaskTemplate.ContainerSpec.Mounts,
146-
})
144+
return checkHostConfigBindMounts(
145+
allowedBindMounts,
146+
&containerHostConfig{
147+
Mounts: req.TaskTemplate.ContainerSpec.Mounts,
148+
},
149+
)
147150
}
148151

149152
// checkHostConfigBindMounts checks bind mounts in HostConfig.
150-
func checkHostConfigBindMounts(hostConfig *containerHostConfig) error {
153+
func checkHostConfigBindMounts(allowedBindMounts []string, hostConfig *containerHostConfig) error {
151154
if hostConfig == nil {
152155
return nil // No HostConfig, nothing to check
153156
}
154157

155158
// Check legacy Binds field
156159
for _, bind := range hostConfig.Binds {
157-
if err := validateBindMount(bind); err != nil {
160+
if err := validateBindMount(allowedBindMounts, bind); err != nil {
158161
return err
159162
}
160163
}
161164

162165
// Check modern Mounts field
163166
for _, mountItem := range hostConfig.Mounts {
164167
if mountItem.Type == mountTypeBind {
165-
if err := validateBindMountSource(mountItem.Source); err != nil {
168+
if err := validateBindMountSource(allowedBindMounts, mountItem.Source); err != nil {
166169
return err
167170
}
168171
}
@@ -172,23 +175,23 @@ func checkHostConfigBindMounts(hostConfig *containerHostConfig) error {
172175
}
173176

174177
// validateBindMount validates a bind mount string in the format "source:target:options".
175-
func validateBindMount(bind string) error {
178+
func validateBindMount(allowedBindMounts []string, bind string) error {
176179
parts := strings.Split(bind, ":")
177180
if len(parts) < 2 {
178181
return fmt.Errorf("invalid bind mount format: %s", bind)
179182
}
180-
return validateBindMountSource(parts[0])
183+
return validateBindMountSource(allowedBindMounts, parts[0])
181184
}
182185

183186
// validateBindMountSource checks if the source directory is allowed.
184-
func validateBindMountSource(source string) error {
187+
func validateBindMountSource(allowedBindMounts []string, source string) error {
185188
// Skip if source is not an absolute path (i.e. bind mount).
186189
if !strings.HasPrefix(source, "/") {
187190
return nil
188191
}
189192

190193
source = filepath.Clean(source) // Clean the path to resolve .. and . components.
191-
for _, allowedDir := range cfg.AllowBindMountFrom {
194+
for _, allowedDir := range allowedBindMounts {
192195
if allowedDir == "/" || source == allowedDir || strings.HasPrefix(source, allowedDir+"/") {
193196
return nil
194197
}

cmd/socket-proxy/bindmount_test.go

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"net/http"
66
"runtime"
77
"testing"
8-
9-
"github.com/wollomatic/socket-proxy/internal/config"
108
)
119

1210
func skipIfNotUnix(t *testing.T) {
@@ -21,9 +19,7 @@ func skipIfNotUnix(t *testing.T) {
2119
func TestValidateBindMountSource(t *testing.T) {
2220
skipIfNotUnix(t)
2321

24-
cfg = &config.Config{
25-
AllowBindMountFrom: []string{"/home", "/var/log"},
26-
}
22+
allowedBindMounts := []string{"/home", "/var/log"}
2723

2824
tests := []struct {
2925
name string
@@ -44,7 +40,7 @@ func TestValidateBindMountSource(t *testing.T) {
4440

4541
for _, tt := range tests {
4642
t.Run(tt.name, func(t *testing.T) {
47-
err := validateBindMountSource(tt.source)
43+
err := validateBindMountSource(allowedBindMounts, tt.source)
4844
if tt.shouldPass && err != nil {
4945
t.Errorf("expected %s to pass, but got error: %v", tt.source, err)
5046
}
@@ -83,10 +79,7 @@ func TestIsPathAllowed(t *testing.T) {
8379

8480
for _, tt := range tests {
8581
t.Run(tt.name, func(t *testing.T) {
86-
cfg = &config.Config{
87-
AllowBindMountFrom: []string{tt.allowedDir},
88-
}
89-
err := validateBindMountSource(tt.path)
82+
err := validateBindMountSource([]string{tt.allowedDir}, tt.path)
9083
if (err == nil) != tt.expected {
9184
t.Errorf("isPathAllowed(%s, %s) = %v, expected %v", tt.path, tt.allowedDir, err, tt.expected)
9285
}
@@ -97,9 +90,7 @@ func TestIsPathAllowed(t *testing.T) {
9790
func TestValidateBindMount(t *testing.T) {
9891
skipIfNotUnix(t)
9992

100-
cfg = &config.Config{
101-
AllowBindMountFrom: []string{"/home", "/var/log"},
102-
}
93+
allowedBindMounts := []string{"/home", "/var/log"}
10394

10495
tests := []struct {
10596
name string
@@ -115,7 +106,7 @@ func TestValidateBindMount(t *testing.T) {
115106

116107
for _, tt := range tests {
117108
t.Run(tt.name, func(t *testing.T) {
118-
err := validateBindMount(tt.bind)
109+
err := validateBindMount(allowedBindMounts, tt.bind)
119110
if tt.shouldPass && err != nil {
120111
t.Errorf("expected %s to pass, but got error: %v", tt.bind, err)
121112
}
@@ -129,9 +120,7 @@ func TestValidateBindMount(t *testing.T) {
129120
func TestCheckBindMountRestrictions(t *testing.T) {
130121
skipIfNotUnix(t)
131122

132-
cfg = &config.Config{
133-
AllowBindMountFrom: []string{"/home"},
134-
}
123+
allowedBindMounts := []string{"/home"}
135124

136125
tests := []struct {
137126
name string
@@ -212,7 +201,7 @@ func TestCheckBindMountRestrictions(t *testing.T) {
212201
t.Fatalf("failed to create request: %v", err)
213202
}
214203

215-
err = checkBindMountRestrictions(req)
204+
err = checkBindMountRestrictions(allowedBindMounts, req)
216205
if tt.shouldPass && err != nil {
217206
t.Errorf("expected request to pass, but got error: %v", err)
218207
}

cmd/socket-proxy/handlehttprequest.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,21 @@ import (
55
"log/slog"
66
"net"
77
"net/http"
8+
9+
"github.com/wollomatic/socket-proxy/internal/config"
810
)
911

1012
// handleHTTPRequest checks if the request is allowed and sends it to the proxy.
1113
// Otherwise, it returns a "405 Method Not Allowed" or a "403 Forbidden" error.
1214
// In case of an error, it returns a 500 Internal Server Error.
1315
func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
14-
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
15-
allowedIP, err := isAllowedClient(r.RemoteAddr)
16-
if err != nil {
17-
slog.Warn("cannot get valid IP address for client allowlist check", "reason", err, "method", r.Method, "URL", r.URL, "client", r.RemoteAddr)
18-
}
19-
if !allowedIP {
20-
communicateBlockedRequest(w, r, "forbidden IP", http.StatusForbidden)
21-
return
22-
}
16+
allowList, ok := determineAllowList(r)
17+
if !ok {
18+
communicateBlockedRequest(w, r, "forbidden IP", http.StatusForbidden)
19+
return
2320
}
2421

25-
// check if the request is allowed
26-
allowed, exists := cfg.AllowedRequests[r.Method]
22+
allowed, exists := allowList.AllowedRequests[r.Method]
2723
if !exists { // method not in map -> not allowed
2824
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
2925
return
@@ -34,7 +30,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
3430
}
3531

3632
// check bind mount restrictions
37-
if err := checkBindMountRestrictions(r); err != nil {
33+
if err := checkBindMountRestrictions(allowList.AllowedBindMounts, r); err != nil {
3834
communicateBlockedRequest(w, r, "bind mount restriction: "+err.Error(), http.StatusForbidden)
3935
return
4036
}
@@ -44,14 +40,40 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
4440
socketProxy.ServeHTTP(w, r) // proxy the request
4541
}
4642

43+
// return the relevant allowlist
44+
func determineAllowList(r *http.Request) (config.AllowList, bool) {
45+
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
46+
// Get the client IP address from the remote address string
47+
clientIPStr, _, err := net.SplitHostPort(r.RemoteAddr)
48+
if err != nil {
49+
slog.Warn("cannot get valid IP address from request", "reason", err, "method", r.Method, "URL", r.URL, "client", r.RemoteAddr)
50+
return config.AllowList{}, false
51+
}
52+
53+
// If applicable, get the non-default allowlist corresponding to the client IP address
54+
if cfg.ProxyContainerName != "" {
55+
allowList, found := cfg.AllowLists.FindByIP(clientIPStr)
56+
if found {
57+
return allowList, true
58+
}
59+
}
60+
61+
// Check if client is allowed for the default allowlist:
62+
allowedIP, err := isAllowedClient(clientIPStr)
63+
if err != nil {
64+
slog.Warn("cannot get valid IP address for client allowlist check", "reason", err, "method", r.Method, "URL", r.URL, "client", r.RemoteAddr)
65+
}
66+
if !allowedIP {
67+
return config.AllowList{}, false
68+
}
69+
}
70+
71+
return cfg.AllowLists.Default, true
72+
}
73+
4774
// isAllowedClient checks if the given remote address is allowed to connect to the proxy.
4875
// The IP address is extracted from a RemoteAddr string (the part before the colon).
49-
func isAllowedClient(remoteAddr string) (bool, error) {
50-
// Get the client IP address from the remote address string
51-
clientIPStr, _, err := net.SplitHostPort(remoteAddr)
52-
if err != nil {
53-
return false, err
54-
}
76+
func isAllowedClient(clientIPStr string) (bool, error) {
5577
// Parse the IP address
5678
clientIP := net.ParseIP(clientIPStr)
5779
if clientIP == nil {

cmd/socket-proxy/main.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ func main() {
5656
}
5757
slog.SetDefault(logger)
5858

59+
// setup non-default allowlists
60+
if cfg.ProxySocketEndpoint == "" && cfg.ProxyContainerName != "" {
61+
go cfg.UpdateAllowLists()
62+
}
63+
5964
// print configuration
6065
slog.Info("starting socket-proxy", "version", version, "os", runtime.GOOS, "arch", runtime.GOARCH, "runtime", runtime.Version(), "URL", programURL)
6166
if cfg.ProxySocketEndpoint == "" {
@@ -71,26 +76,17 @@ func main() {
7176
} else {
7277
slog.Info("watchdog disabled")
7378
}
74-
if len(cfg.AllowBindMountFrom) > 0 {
75-
slog.Info("Docker bind mount restrictions enabled", "allowbindmountfrom", cfg.AllowBindMountFrom)
79+
if len(cfg.ProxyContainerName) > 0 {
80+
slog.Info("Proxy container name provided", "proxycontainername", cfg.ProxyContainerName)
7681
} else {
77-
// we only log this on DEBUG level because bind mount restrictions are a very special use case
78-
slog.Debug("no Docker bind mount restrictions")
82+
// we only log this on DEBUG level because providing the socket-proxy container name
83+
// enables the use of labels to specify per-container allowlists
84+
slog.Debug("no proxy container name provided")
7985
}
86+
cfg.AllowLists.PrintNetworks()
8087

81-
// print request allowlist
82-
if cfg.LogJSON {
83-
for method, regex := range cfg.AllowedRequests {
84-
slog.Info("configured allowed request", "method", method, "regex", regex)
85-
}
86-
} else {
87-
// don't use slog here, as we want to print the regexes as they are
88-
// see https://github.com/wollomatic/socket-proxy/issues/11
89-
fmt.Printf("Request allowlist:\n %-8s %s\n", "Method", "Regex")
90-
for method, regex := range cfg.AllowedRequests {
91-
fmt.Printf(" %-8s %s\n", method, regex)
92-
}
93-
}
88+
// print default request allowlist
89+
cfg.AllowLists.PrintDefault(cfg.LogJSON)
9490

9591
// check if the socket is available
9692
err = checkSocketAvailability(cfg.SocketPath)

0 commit comments

Comments
 (0)