Skip to content
9 changes: 7 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

> [!NOTE]
> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.
> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO, version


## Getting Started

Expand DownExpand Up@@ -93,10 +94,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins
Examples (command-line):
+ `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.
+ `'-allowHEAD=.*'` allows all HEAD requests.
+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times

Examples (env variables):
+ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2.
+ `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests.
+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times

For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).

Expand DownExpand Up@@ -135,6 +138,8 @@ services:
- docker-proxynet # this should be only restricted to traefik and socket-proxy
labels:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand DownExpand Up@@ -227,15 +232,15 @@ To log the API calls of the client application, set the log level to `DEBUG` and
socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.

| Parameter | Environment Variable | Default Value | Description |
|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------ | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. |
| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) |
| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. |
| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) |
| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) |
Expand Down
12 changes: 11 additions & 1 deletion cmd/socket-proxy/handlehttprequest.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"regexp"

"github.com/wollomatic/socket-proxy/internal/config"
)
Expand All@@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed
if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed
communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden)
return
}
Expand All@@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {
for _, allowedURI := range allowedURIs {
if allowedURI.MatchString(requestURI) {
return true
}
}
return false
}

// return the relevant allowlist
func determineAllowList(r *http.Request) (config.AllowList, bool) {
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
Expand Down
69 changes: 40 additions & 29 deletions internal/config/config.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,22 +67,20 @@ type AllowListRegistry struct {
}

type AllowList struct {
ID string // Container ID (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)
ID string // Container ID (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)
}

// used for list of allowed requests
type methodRegex struct {
method string
regexStringFromEnv string
regexStringFromParam string
method string
regexStrings arrayParams
}

// mr is the allowlist of requests per http method
// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked
// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed
// else if regexStringEnv from Environment ist checked
// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked
// if regexStrings is set, all requests matching the method and path matching the regex are allowed
var mr = []methodRegex{
{method: http.MethodGet},
{method: http.MethodHead},
Expand DownExpand Up@@ -163,9 +161,14 @@ func InitConfig() (*Config, error) {
defaultProxyContainerName = val
}

// multiple values per method
// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...
allowFromEnv := getAllowFromEnv(os.Environ())
for i := range mr {
if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" {
mr[i].regexStringFromEnv = val
if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 {
for _, v := range val {
mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv})
}
}
}
Comment thread
qianlongzt marked this conversation as resolved.

Expand All@@ -190,7 +193,7 @@ func InitConfig() (*Config, error) {
flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)")
flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name")
for i := range mr {
flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)")
flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)")
}
flag.Parse()

Expand DownExpand Up@@ -245,20 +248,23 @@ func InitConfig() (*Config, error) {
cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))

// compile regexes for default allowed requests
cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp)
cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)
for _, rx := range mr {
if rx.regexStringFromParam != "" {
r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter")
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
} else if rx.regexStringFromEnv != "" {
r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable")
if err != nil {
return nil, err
for _, regexString := range rx.regexStrings {
if regexString.value != "" {
location := ""
switch regexString.from {
case fromEnv:
location = "env variable"
case fromParam:
location = "command line parameter"
}
r, err := compileRegexp(regexString.value, rx.method, location)
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
}
}

Expand DownExpand Up@@ -634,18 +640,23 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont
}

// extract Docker container allowlist label data from the container summary
func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string]*regexp.Regexp)
func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string][]*regexp.Regexp)
var allowedBindMounts []string
for labelName, labelValue := range cntr.Labels {
if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" {
allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))
if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) {
r, err := compileRegexp(labelValue, allowSpec, "docker container label")
if slices.ContainsFunc(mr, func(rx methodRegex) bool {
// allowSpec starts with the method name like socket-proxy.allow.get.1
return strings.HasPrefix(allowSpec, rx.method)
}) {
// extract the method name from allowSpec
method, _, _ := strings.Cut(allowSpec, ".")
r, err := compileRegexp(labelValue, method, "docker container label")
if err != nil {
return nil, nil, err
}
allowedRequests[allowSpec] = r
allowedRequests[method] = append(allowedRequests[method], r)
} else if allowSpec == "BINDMOUNTFROM" {
var err error
allowedBindMounts, err = parseAllowedBindMounts(labelValue)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: support use allow* multiple times in env, flag and docker labels by qianlongzt · Pull Request #86 · wollomatic/socket-proxy · GitHub
Skip to content
9 changes: 7 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

> [!NOTE]
> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.
> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO, version


## Getting Started

Expand DownExpand Up@@ -93,10 +94,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins
Examples (command-line):
+ `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.
+ `'-allowHEAD=.*'` allows all HEAD requests.
+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times

Examples (env variables):
+ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2.
+ `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests.
+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times

For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).

Expand DownExpand Up@@ -135,6 +138,8 @@ services:
- docker-proxynet # this should be only restricted to traefik and socket-proxy
labels:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand DownExpand Up@@ -227,15 +232,15 @@ To log the API calls of the client application, set the log level to `DEBUG` and
socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.

| Parameter | Environment Variable | Default Value | Description |
|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------ | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. |
| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) |
| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. |
| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) |
| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) |
Expand Down
12 changes: 11 additions & 1 deletion cmd/socket-proxy/handlehttprequest.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"regexp"

"github.com/wollomatic/socket-proxy/internal/config"
)
Expand All@@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed
if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed
communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden)
return
}
Expand All@@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {
for _, allowedURI := range allowedURIs {
if allowedURI.MatchString(requestURI) {
return true
}
}
return false
}

// return the relevant allowlist
func determineAllowList(r *http.Request) (config.AllowList, bool) {
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
Expand Down
69 changes: 40 additions & 29 deletions internal/config/config.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,22 +67,20 @@ type AllowListRegistry struct {
}

type AllowList struct {
ID string // Container ID (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)
ID string // Container ID (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)
}

// used for list of allowed requests
type methodRegex struct {
method string
regexStringFromEnv string
regexStringFromParam string
method string
regexStrings arrayParams
}

// mr is the allowlist of requests per http method
// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked
// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed
// else if regexStringEnv from Environment ist checked
// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked
// if regexStrings is set, all requests matching the method and path matching the regex are allowed
var mr = []methodRegex{
{method: http.MethodGet},
{method: http.MethodHead},
Expand DownExpand Up@@ -163,9 +161,14 @@ func InitConfig() (*Config, error) {
defaultProxyContainerName = val
}

// multiple values per method
// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...
allowFromEnv := getAllowFromEnv(os.Environ())
for i := range mr {
if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" {
mr[i].regexStringFromEnv = val
if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 {
for _, v := range val {
mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv})
}
}
}
Comment thread
qianlongzt marked this conversation as resolved.

Expand All@@ -190,7 +193,7 @@ func InitConfig() (*Config, error) {
flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)")
flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name")
for i := range mr {
flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)")
flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)")
}
flag.Parse()

Expand DownExpand Up@@ -245,20 +248,23 @@ func InitConfig() (*Config, error) {
cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))

// compile regexes for default allowed requests
cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp)
cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)
for _, rx := range mr {
if rx.regexStringFromParam != "" {
r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter")
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
} else if rx.regexStringFromEnv != "" {
r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable")
if err != nil {
return nil, err
for _, regexString := range rx.regexStrings {
if regexString.value != "" {
location := ""
switch regexString.from {
case fromEnv:
location = "env variable"
case fromParam:
location = "command line parameter"
}
r, err := compileRegexp(regexString.value, rx.method, location)
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
}
}

Expand DownExpand Up@@ -634,18 +640,23 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont
}

// extract Docker container allowlist label data from the container summary
func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string]*regexp.Regexp)
func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string][]*regexp.Regexp)
var allowedBindMounts []string
for labelName, labelValue := range cntr.Labels {
if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" {
allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))
if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) {
r, err := compileRegexp(labelValue, allowSpec, "docker container label")
if slices.ContainsFunc(mr, func(rx methodRegex) bool {
// allowSpec starts with the method name like socket-proxy.allow.get.1
return strings.HasPrefix(allowSpec, rx.method)
}) {
// extract the method name from allowSpec
method, _, _ := strings.Cut(allowSpec, ".")
r, err := compileRegexp(labelValue, method, "docker container label")
if err != nil {
return nil, nil, err
}
allowedRequests[allowSpec] = r
allowedRequests[method] = append(allowedRequests[method], r)
} else if allowSpec == "BINDMOUNTFROM" {
var err error
allowedBindMounts, err = parseAllowedBindMounts(labelValue)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: support use allow* multiple times in env, flag and docker labels by qianlongzt · Pull Request #86 · wollomatic/socket-proxy · GitHub
Skip to content
9 changes: 7 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

> [!NOTE]
> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.
> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO, version


## Getting Started

Expand DownExpand Up@@ -93,10 +94,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins
Examples (command-line):
+ `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.
+ `'-allowHEAD=.*'` allows all HEAD requests.
+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times

Examples (env variables):
+ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2.
+ `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests.
+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times

For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).

Expand DownExpand Up@@ -135,6 +138,8 @@ services:
- docker-proxynet # this should be only restricted to traefik and socket-proxy
labels:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand DownExpand Up@@ -227,15 +232,15 @@ To log the API calls of the client application, set the log level to `DEBUG` and
socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.

| Parameter | Environment Variable | Default Value | Description |
|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------ | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. |
| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) |
| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. |
| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) |
| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) |
Expand Down
12 changes: 11 additions & 1 deletion cmd/socket-proxy/handlehttprequest.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"regexp"

"github.com/wollomatic/socket-proxy/internal/config"
)
Expand All@@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed
if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed
communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden)
return
}
Expand All@@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {
for _, allowedURI := range allowedURIs {
if allowedURI.MatchString(requestURI) {
return true
}
}
return false
}

// return the relevant allowlist
func determineAllowList(r *http.Request) (config.AllowList, bool) {
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
Expand Down
69 changes: 40 additions & 29 deletions internal/config/config.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,22 +67,20 @@ type AllowListRegistry struct {
}

type AllowList struct {
ID string // Container ID (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)
ID string // Container ID (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)
}

// used for list of allowed requests
type methodRegex struct {
method string
regexStringFromEnv string
regexStringFromParam string
method string
regexStrings arrayParams
}

// mr is the allowlist of requests per http method
// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked
// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed
// else if regexStringEnv from Environment ist checked
// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked
// if regexStrings is set, all requests matching the method and path matching the regex are allowed
var mr = []methodRegex{
{method: http.MethodGet},
{method: http.MethodHead},
Expand DownExpand Up@@ -163,9 +161,14 @@ func InitConfig() (*Config, error) {
defaultProxyContainerName = val
}

// multiple values per method
// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...
allowFromEnv := getAllowFromEnv(os.Environ())
for i := range mr {
if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" {
mr[i].regexStringFromEnv = val
if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 {
for _, v := range val {
mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv})
}
}
}
Comment thread
qianlongzt marked this conversation as resolved.

Expand All@@ -190,7 +193,7 @@ func InitConfig() (*Config, error) {
flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)")
flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name")
for i := range mr {
flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)")
flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)")
}
flag.Parse()

Expand DownExpand Up@@ -245,20 +248,23 @@ func InitConfig() (*Config, error) {
cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))

// compile regexes for default allowed requests
cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp)
cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)
for _, rx := range mr {
if rx.regexStringFromParam != "" {
r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter")
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
} else if rx.regexStringFromEnv != "" {
r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable")
if err != nil {
return nil, err
for _, regexString := range rx.regexStrings {
if regexString.value != "" {
location := ""
switch regexString.from {
case fromEnv:
location = "env variable"
case fromParam:
location = "command line parameter"
}
r, err := compileRegexp(regexString.value, rx.method, location)
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
}
}

Expand DownExpand Up@@ -634,18 +640,23 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont
}

// extract Docker container allowlist label data from the container summary
func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string]*regexp.Regexp)
func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string][]*regexp.Regexp)
var allowedBindMounts []string
for labelName, labelValue := range cntr.Labels {
if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" {
allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))
if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) {
r, err := compileRegexp(labelValue, allowSpec, "docker container label")
if slices.ContainsFunc(mr, func(rx methodRegex) bool {
// allowSpec starts with the method name like socket-proxy.allow.get.1
return strings.HasPrefix(allowSpec, rx.method)
}) {
// extract the method name from allowSpec
method, _, _ := strings.Cut(allowSpec, ".")
r, err := compileRegexp(labelValue, method, "docker container label")
if err != nil {
return nil, nil, err
}
allowedRequests[allowSpec] = r
allowedRequests[method] = append(allowedRequests[method], r)
} else if allowSpec == "BINDMOUNTFROM" {
var err error
allowedBindMounts, err = parseAllowedBindMounts(labelValue)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: support use allow* multiple times in env, flag and docker labels by qianlongzt · Pull Request #86 · wollomatic/socket-proxy · GitHub
Skip to content
9 changes: 7 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

> [!NOTE]
> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.
> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO, version


## Getting Started

Expand DownExpand Up@@ -93,10 +94,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins
Examples (command-line):
+ `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.
+ `'-allowHEAD=.*'` allows all HEAD requests.
+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times

Examples (env variables):
+ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2.
+ `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests.
+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times

For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).

Expand DownExpand Up@@ -135,6 +138,8 @@ services:
- docker-proxynet # this should be only restricted to traefik and socket-proxy
labels:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand DownExpand Up@@ -227,15 +232,15 @@ To log the API calls of the client application, set the log level to `DEBUG` and
socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.

| Parameter | Environment Variable | Default Value | Description |
|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------ | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. |
| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) |
| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. |
| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) |
| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) |
Expand Down
12 changes: 11 additions & 1 deletion cmd/socket-proxy/handlehttprequest.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"regexp"

"github.com/wollomatic/socket-proxy/internal/config"
)
Expand All@@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed
if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed
communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden)
return
}
Expand All@@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {
for _, allowedURI := range allowedURIs {
if allowedURI.MatchString(requestURI) {
return true
}
}
return false
}

// return the relevant allowlist
func determineAllowList(r *http.Request) (config.AllowList, bool) {
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
Expand Down
69 changes: 40 additions & 29 deletions internal/config/config.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,22 +67,20 @@ type AllowListRegistry struct {
}

type AllowList struct {
ID string // Container ID (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)
ID string // Container ID (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)
}

// used for list of allowed requests
type methodRegex struct {
method string
regexStringFromEnv string
regexStringFromParam string
method string
regexStrings arrayParams
}

// mr is the allowlist of requests per http method
// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked
// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed
// else if regexStringEnv from Environment ist checked
// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked
// if regexStrings is set, all requests matching the method and path matching the regex are allowed
var mr = []methodRegex{
{method: http.MethodGet},
{method: http.MethodHead},
Expand DownExpand Up@@ -163,9 +161,14 @@ func InitConfig() (*Config, error) {
defaultProxyContainerName = val
}

// multiple values per method
// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...
allowFromEnv := getAllowFromEnv(os.Environ())
for i := range mr {
if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" {
mr[i].regexStringFromEnv = val
if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 {
for _, v := range val {
mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv})
}
}
}
Comment thread
qianlongzt marked this conversation as resolved.

Expand All@@ -190,7 +193,7 @@ func InitConfig() (*Config, error) {
flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)")
flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name")
for i := range mr {
flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)")
flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)")
}
flag.Parse()

Expand DownExpand Up@@ -245,20 +248,23 @@ func InitConfig() (*Config, error) {
cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))

// compile regexes for default allowed requests
cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp)
cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)
for _, rx := range mr {
if rx.regexStringFromParam != "" {
r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter")
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
} else if rx.regexStringFromEnv != "" {
r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable")
if err != nil {
return nil, err
for _, regexString := range rx.regexStrings {
if regexString.value != "" {
location := ""
switch regexString.from {
case fromEnv:
location = "env variable"
case fromParam:
location = "command line parameter"
}
r, err := compileRegexp(regexString.value, rx.method, location)
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
}
}

Expand DownExpand Up@@ -634,18 +640,23 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont
}

// extract Docker container allowlist label data from the container summary
func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string]*regexp.Regexp)
func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string][]*regexp.Regexp)
var allowedBindMounts []string
for labelName, labelValue := range cntr.Labels {
if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" {
allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))
if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) {
r, err := compileRegexp(labelValue, allowSpec, "docker container label")
if slices.ContainsFunc(mr, func(rx methodRegex) bool {
// allowSpec starts with the method name like socket-proxy.allow.get.1
return strings.HasPrefix(allowSpec, rx.method)
}) {
// extract the method name from allowSpec
method, _, _ := strings.Cut(allowSpec, ".")
r, err := compileRegexp(labelValue, method, "docker container label")
if err != nil {
return nil, nil, err
}
allowedRequests[allowSpec] = r
allowedRequests[method] = append(allowedRequests[method], r)
} else if allowSpec == "BINDMOUNTFROM" {
var err error
allowedBindMounts, err = parseAllowedBindMounts(labelValue)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: support use allow* multiple times in env, flag and docker labels by qianlongzt · Pull Request #86 · wollomatic/socket-proxy · GitHub
Skip to content
9 changes: 7 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

> [!NOTE]
> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.
> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO, version


## Getting Started

Expand DownExpand Up@@ -93,10 +94,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins
Examples (command-line):
+ `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.
+ `'-allowHEAD=.*'` allows all HEAD requests.
+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times

Examples (env variables):
+ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2.
+ `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests.
+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times

For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).

Expand DownExpand Up@@ -135,6 +138,8 @@ services:
- docker-proxynet # this should be only restricted to traefik and socket-proxy
labels:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand DownExpand Up@@ -227,15 +232,15 @@ To log the API calls of the client application, set the log level to `DEBUG` and
socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.

| Parameter | Environment Variable | Default Value | Description |
|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------ | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. |
| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) |
| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. |
| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) |
| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) |
Expand Down
12 changes: 11 additions & 1 deletion cmd/socket-proxy/handlehttprequest.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"regexp"

"github.com/wollomatic/socket-proxy/internal/config"
)
Expand All@@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed
if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed
communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden)
return
}
Expand All@@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {
for _, allowedURI := range allowedURIs {
if allowedURI.MatchString(requestURI) {
return true
}
}
return false
}

// return the relevant allowlist
func determineAllowList(r *http.Request) (config.AllowList, bool) {
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
Expand Down
69 changes: 40 additions & 29 deletions internal/config/config.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,22 +67,20 @@ type AllowListRegistry struct {
}

type AllowList struct {
ID string // Container ID (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)
ID string // Container ID (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)
}

// used for list of allowed requests
type methodRegex struct {
method string
regexStringFromEnv string
regexStringFromParam string
method string
regexStrings arrayParams
}

// mr is the allowlist of requests per http method
// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked
// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed
// else if regexStringEnv from Environment ist checked
// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked
// if regexStrings is set, all requests matching the method and path matching the regex are allowed
var mr = []methodRegex{
{method: http.MethodGet},
{method: http.MethodHead},
Expand DownExpand Up@@ -163,9 +161,14 @@ func InitConfig() (*Config, error) {
defaultProxyContainerName = val
}

// multiple values per method
// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...
allowFromEnv := getAllowFromEnv(os.Environ())
for i := range mr {
if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" {
mr[i].regexStringFromEnv = val
if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 {
for _, v := range val {
mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv})
}
}
}
Comment thread
qianlongzt marked this conversation as resolved.

Expand All@@ -190,7 +193,7 @@ func InitConfig() (*Config, error) {
flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)")
flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name")
for i := range mr {
flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)")
flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)")
}
flag.Parse()

Expand DownExpand Up@@ -245,20 +248,23 @@ func InitConfig() (*Config, error) {
cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))

// compile regexes for default allowed requests
cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp)
cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)
for _, rx := range mr {
if rx.regexStringFromParam != "" {
r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter")
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
} else if rx.regexStringFromEnv != "" {
r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable")
if err != nil {
return nil, err
for _, regexString := range rx.regexStrings {
if regexString.value != "" {
location := ""
switch regexString.from {
case fromEnv:
location = "env variable"
case fromParam:
location = "command line parameter"
}
r, err := compileRegexp(regexString.value, rx.method, location)
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
}
}

Expand DownExpand Up@@ -634,18 +640,23 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont
}

// extract Docker container allowlist label data from the container summary
func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string]*regexp.Regexp)
func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string][]*regexp.Regexp)
var allowedBindMounts []string
for labelName, labelValue := range cntr.Labels {
if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" {
allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))
if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) {
r, err := compileRegexp(labelValue, allowSpec, "docker container label")
if slices.ContainsFunc(mr, func(rx methodRegex) bool {
// allowSpec starts with the method name like socket-proxy.allow.get.1
return strings.HasPrefix(allowSpec, rx.method)
}) {
// extract the method name from allowSpec
method, _, _ := strings.Cut(allowSpec, ".")
r, err := compileRegexp(labelValue, method, "docker container label")
if err != nil {
return nil, nil, err
}
allowedRequests[allowSpec] = r
allowedRequests[method] = append(allowedRequests[method], r)
} else if allowSpec == "BINDMOUNTFROM" {
var err error
allowedBindMounts, err = parseAllowedBindMounts(labelValue)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: support use allow* multiple times in env, flag and docker labels by qianlongzt · Pull Request #86 · wollomatic/socket-proxy · GitHub
Skip to content
9 changes: 7 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

> [!NOTE]
> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.
> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO, version


## Getting Started

Expand DownExpand Up@@ -93,10 +94,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins
Examples (command-line):
+ `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.
+ `'-allowHEAD=.*'` allows all HEAD requests.
+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times

Examples (env variables):
+ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2.
+ `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests.
+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times

For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).

Expand DownExpand Up@@ -135,6 +138,8 @@ services:
- docker-proxynet # this should be only restricted to traefik and socket-proxy
labels:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand DownExpand Up@@ -227,15 +232,15 @@ To log the API calls of the client application, set the log level to `DEBUG` and
socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.

| Parameter | Environment Variable | Default Value | Description |
|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------ | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. |
| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) |
| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. |
| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) |
| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) |
Expand Down
12 changes: 11 additions & 1 deletion cmd/socket-proxy/handlehttprequest.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"regexp"

"github.com/wollomatic/socket-proxy/internal/config"
)
Expand All@@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed
if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed
communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden)
return
}
Expand All@@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {
for _, allowedURI := range allowedURIs {
if allowedURI.MatchString(requestURI) {
return true
}
}
return false
}

// return the relevant allowlist
func determineAllowList(r *http.Request) (config.AllowList, bool) {
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
Expand Down
69 changes: 40 additions & 29 deletions internal/config/config.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,22 +67,20 @@ type AllowListRegistry struct {
}

type AllowList struct {
ID string // Container ID (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)
ID string // Container ID (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)
}

// used for list of allowed requests
type methodRegex struct {
method string
regexStringFromEnv string
regexStringFromParam string
method string
regexStrings arrayParams
}

// mr is the allowlist of requests per http method
// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked
// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed
// else if regexStringEnv from Environment ist checked
// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked
// if regexStrings is set, all requests matching the method and path matching the regex are allowed
var mr = []methodRegex{
{method: http.MethodGet},
{method: http.MethodHead},
Expand DownExpand Up@@ -163,9 +161,14 @@ func InitConfig() (*Config, error) {
defaultProxyContainerName = val
}

// multiple values per method
// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...
allowFromEnv := getAllowFromEnv(os.Environ())
for i := range mr {
if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" {
mr[i].regexStringFromEnv = val
if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 {
for _, v := range val {
mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv})
}
}
}
Comment thread
qianlongzt marked this conversation as resolved.

Expand All@@ -190,7 +193,7 @@ func InitConfig() (*Config, error) {
flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)")
flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name")
for i := range mr {
flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)")
flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)")
}
flag.Parse()

Expand DownExpand Up@@ -245,20 +248,23 @@ func InitConfig() (*Config, error) {
cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))

// compile regexes for default allowed requests
cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp)
cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)
for _, rx := range mr {
if rx.regexStringFromParam != "" {
r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter")
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
} else if rx.regexStringFromEnv != "" {
r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable")
if err != nil {
return nil, err
for _, regexString := range rx.regexStrings {
if regexString.value != "" {
location := ""
switch regexString.from {
case fromEnv:
location = "env variable"
case fromParam:
location = "command line parameter"
}
r, err := compileRegexp(regexString.value, rx.method, location)
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
}
}

Expand DownExpand Up@@ -634,18 +640,23 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont
}

// extract Docker container allowlist label data from the container summary
func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string]*regexp.Regexp)
func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string][]*regexp.Regexp)
var allowedBindMounts []string
for labelName, labelValue := range cntr.Labels {
if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" {
allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))
if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) {
r, err := compileRegexp(labelValue, allowSpec, "docker container label")
if slices.ContainsFunc(mr, func(rx methodRegex) bool {
// allowSpec starts with the method name like socket-proxy.allow.get.1
return strings.HasPrefix(allowSpec, rx.method)
}) {
// extract the method name from allowSpec
method, _, _ := strings.Cut(allowSpec, ".")
r, err := compileRegexp(labelValue, method, "docker container label")
if err != nil {
return nil, nil, err
}
allowedRequests[allowSpec] = r
allowedRequests[method] = append(allowedRequests[method], r)
} else if allowSpec == "BINDMOUNTFROM" {
var err error
allowedBindMounts, err = parseAllowedBindMounts(labelValue)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: support use allow* multiple times in env, flag and docker labels by qianlongzt · Pull Request #86 · wollomatic/socket-proxy · GitHub
Skip to content
9 changes: 7 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

> [!NOTE]
> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.
> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO, version


## Getting Started

Expand DownExpand Up@@ -93,10 +94,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins
Examples (command-line):
+ `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.
+ `'-allowHEAD=.*'` allows all HEAD requests.
+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times

Examples (env variables):
+ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2.
+ `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests.
+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times

For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).

Expand DownExpand Up@@ -135,6 +138,8 @@ services:
- docker-proxynet # this should be only restricted to traefik and socket-proxy
labels:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand DownExpand Up@@ -227,15 +232,15 @@ To log the API calls of the client application, set the log level to `DEBUG` and
socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.

| Parameter | Environment Variable | Default Value | Description |
|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------ | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. |
| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) |
| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. |
| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) |
| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) |
Expand Down
12 changes: 11 additions & 1 deletion cmd/socket-proxy/handlehttprequest.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"regexp"

"github.com/wollomatic/socket-proxy/internal/config"
)
Expand All@@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed
if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed
communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden)
return
}
Expand All@@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {
for _, allowedURI := range allowedURIs {
if allowedURI.MatchString(requestURI) {
return true
}
}
return false
}

// return the relevant allowlist
func determineAllowList(r *http.Request) (config.AllowList, bool) {
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
Expand Down
69 changes: 40 additions & 29 deletions internal/config/config.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,22 +67,20 @@ type AllowListRegistry struct {
}

type AllowList struct {
ID string // Container ID (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)
ID string // Container ID (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)
}

// used for list of allowed requests
type methodRegex struct {
method string
regexStringFromEnv string
regexStringFromParam string
method string
regexStrings arrayParams
}

// mr is the allowlist of requests per http method
// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked
// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed
// else if regexStringEnv from Environment ist checked
// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked
// if regexStrings is set, all requests matching the method and path matching the regex are allowed
var mr = []methodRegex{
{method: http.MethodGet},
{method: http.MethodHead},
Expand DownExpand Up@@ -163,9 +161,14 @@ func InitConfig() (*Config, error) {
defaultProxyContainerName = val
}

// multiple values per method
// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...
allowFromEnv := getAllowFromEnv(os.Environ())
for i := range mr {
if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" {
mr[i].regexStringFromEnv = val
if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 {
for _, v := range val {
mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv})
}
}
}
Comment thread
qianlongzt marked this conversation as resolved.

Expand All@@ -190,7 +193,7 @@ func InitConfig() (*Config, error) {
flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)")
flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name")
for i := range mr {
flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)")
flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)")
}
flag.Parse()

Expand DownExpand Up@@ -245,20 +248,23 @@ func InitConfig() (*Config, error) {
cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))

// compile regexes for default allowed requests
cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp)
cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)
for _, rx := range mr {
if rx.regexStringFromParam != "" {
r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter")
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
} else if rx.regexStringFromEnv != "" {
r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable")
if err != nil {
return nil, err
for _, regexString := range rx.regexStrings {
if regexString.value != "" {
location := ""
switch regexString.from {
case fromEnv:
location = "env variable"
case fromParam:
location = "command line parameter"
}
r, err := compileRegexp(regexString.value, rx.method, location)
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
}
}

Expand DownExpand Up@@ -634,18 +640,23 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont
}

// extract Docker container allowlist label data from the container summary
func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string]*regexp.Regexp)
func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string][]*regexp.Regexp)
var allowedBindMounts []string
for labelName, labelValue := range cntr.Labels {
if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" {
allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))
if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) {
r, err := compileRegexp(labelValue, allowSpec, "docker container label")
if slices.ContainsFunc(mr, func(rx methodRegex) bool {
// allowSpec starts with the method name like socket-proxy.allow.get.1
return strings.HasPrefix(allowSpec, rx.method)
}) {
// extract the method name from allowSpec
method, _, _ := strings.Cut(allowSpec, ".")
r, err := compileRegexp(labelValue, method, "docker container label")
if err != nil {
return nil, nil, err
}
allowedRequests[allowSpec] = r
allowedRequests[method] = append(allowedRequests[method], r)
} else if allowSpec == "BINDMOUNTFROM" {
var err error
allowedBindMounts, err = parseAllowedBindMounts(labelValue)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: support use allow* multiple times in env, flag and docker labels by qianlongzt · Pull Request #86 · wollomatic/socket-proxy · GitHub
Skip to content
9 changes: 7 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

> [!NOTE]
> Starting with version 1.6.0, the socket-proxy container image is also available on GHCR.
> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO, version


## Getting Started

Expand DownExpand Up@@ -93,10 +94,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins
Examples (command-line):
+ `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2.
+ `'-allowHEAD=.*'` allows all HEAD requests.
+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times

Examples (env variables):
+ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2.
+ `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests.
+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times

For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/).

Expand DownExpand Up@@ -135,6 +138,8 @@ services:
- docker-proxynet # this should be only restricted to traefik and socket-proxy
labels:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand DownExpand Up@@ -227,15 +232,15 @@ To log the API calls of the client application, set the log level to `DEBUG` and
socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored.

| Parameter | Environment Variable | Default Value | Description |
|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------ | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. |
| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) |
| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. |
| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | |
| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) |
| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) |
Expand Down
12 changes: 11 additions & 1 deletion cmd/socket-proxy/handlehttprequest.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"regexp"

"github.com/wollomatic/socket-proxy/internal/config"
)
Expand All@@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed
if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed
communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden)
return
}
Expand All@@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool {
for _, allowedURI := range allowedURIs {
if allowedURI.MatchString(requestURI) {
return true
}
}
return false
}

// return the relevant allowlist
func determineAllowList(r *http.Request) (config.AllowList, bool) {
if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket
Expand Down
69 changes: 40 additions & 29 deletions internal/config/config.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,22 +67,20 @@ type AllowListRegistry struct {
}

type AllowList struct {
ID string // Container ID (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)
ID string // Container ID (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)
}

// used for list of allowed requests
type methodRegex struct {
method string
regexStringFromEnv string
regexStringFromParam string
method string
regexStrings arrayParams
}

// mr is the allowlist of requests per http method
// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked
// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed
// else if regexStringEnv from Environment ist checked
// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked
// if regexStrings is set, all requests matching the method and path matching the regex are allowed
var mr = []methodRegex{
{method: http.MethodGet},
{method: http.MethodHead},
Expand DownExpand Up@@ -163,9 +161,14 @@ func InitConfig() (*Config, error) {
defaultProxyContainerName = val
}

// multiple values per method
// like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ...
allowFromEnv := getAllowFromEnv(os.Environ())
for i := range mr {
if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" {
mr[i].regexStringFromEnv = val
if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 {
for _, v := range val {
mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv})
}
}
}
Comment thread
qianlongzt marked this conversation as resolved.

Expand All@@ -190,7 +193,7 @@ func InitConfig() (*Config, error) {
flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)")
flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name")
for i := range mr {
flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)")
flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)")
}
flag.Parse()

Expand DownExpand Up@@ -245,20 +248,23 @@ func InitConfig() (*Config, error) {
cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode))

// compile regexes for default allowed requests
cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp)
cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp)
for _, rx := range mr {
if rx.regexStringFromParam != "" {
r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter")
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
} else if rx.regexStringFromEnv != "" {
r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable")
if err != nil {
return nil, err
for _, regexString := range rx.regexStrings {
if regexString.value != "" {
location := ""
switch regexString.from {
case fromEnv:
location = "env variable"
case fromParam:
location = "command line parameter"
}
r, err := compileRegexp(regexString.value, rx.method, location)
if err != nil {
return nil, err
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r)
}
cfg.AllowLists.Default.AllowedRequests[rx.method] = r
}
}

Expand DownExpand Up@@ -634,18 +640,23 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont
}

// extract Docker container allowlist label data from the container summary
func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string]*regexp.Regexp)
func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) {
allowedRequests := make(map[string][]*regexp.Regexp)
var allowedBindMounts []string
for labelName, labelValue := range cntr.Labels {
if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" {
allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix))
if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) {
r, err := compileRegexp(labelValue, allowSpec, "docker container label")
if slices.ContainsFunc(mr, func(rx methodRegex) bool {
// allowSpec starts with the method name like socket-proxy.allow.get.1
return strings.HasPrefix(allowSpec, rx.method)
}) {
// extract the method name from allowSpec
method, _, _ := strings.Cut(allowSpec, ".")
r, err := compileRegexp(labelValue, method, "docker container label")
if err != nil {
return nil, nil, err
}
allowedRequests[allowSpec] = r
allowedRequests[method] = append(allowedRequests[method], r)
} else if allowSpec == "BINDMOUNTFROM" {
var err error
allowedBindMounts, err = parseAllowedBindMounts(labelValue)
Expand Down
Loading