Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow hostname for allowfrom #3

Merged
merged 5 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.21.5-alpine AS build
FROM --platform=$BUILDPLATFORM golang:1.21.6-alpine AS build
WORKDIR /application
COPY . ./
ARG TARGETOS
Expand Down
48 changes: 35 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
`socket-proxy` is a lightweight, secure-by-default unix socket proxy. Although it was created to proxy the docker socket to Traefik, it can be also used for other purposes.
It is heavily inspired by [tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy).

As an additional benefit socket-proxy can be used to examine the API calls of the client application.

The advantage over other solutions is the very slim container image ("FROM scratch") without any external dependencies (no OS, no packages, just the Go standard library).
It is designed with security in mind, so there are secure defaults, and there is an extra security layer (IP address-based access control).

Expand All @@ -13,7 +15,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github

## Getting Started

Some examples can be found in the [wiki](https://github.com/wollomatic/socket-proxy/wiki).
Some examples can be found in the [wiki](https://github.com/wollomatic/socket-proxy/wiki) and in the `examples` directory of the repo.

### Warning

Expand Down Expand Up @@ -42,6 +44,8 @@ Socket-proxy listens per default only on `127.0.0.1`. Depending what you need, y

Per default, only `127.0.0.1/32` ist allowed to connect to socket-proxy. Depending on your needs, you may want to set another allowlist with the `-allowfrom` parameter.

Since version 1.1.0, not only IP networks can be configured, but also hostnames. So it is now possible to explicitly allow only one specific client to connect to the proxy, for example `-allowfrom=traefik`

#### Setting up the allowlist for requests

You must set up regular expressions for each HTTP method the client application needs access to.
Expand Down Expand Up @@ -73,7 +77,9 @@ Health checks are disables by default. As the socket-proxy container may not be
retries: 2
# [...]
```
### Socket watchdog

In certain circumstances (for example, after a Docker engine update), the socket connection may break, causing the client application to fail. To prevent this, the socket-proxy can be configured to check the socket availability at regular intervals. If the socket is not available, the socket-proxy will be stopped, so it can be restarted by the container orchestrator. This feature is disabled by default. To enable it, set the `-watchdoginterval` parameter to the desired interval in seconds and set the `-stoponwatchdog` parameter. If `-stoponwatchdog`is not set, the watchdog will only log an error message and continue to run.

### Example for proxying the docker socket to Traefik

Expand Down Expand Up @@ -124,20 +130,36 @@ networks:
internal: true
```

### Examining the API calls of the client application

To just log the API calls of the client application, set the log level to `DEBUG` and allow all requests. Then, you can examine the log output to determine which requests are made by the client application. Allowing all requests can be done by setting the following parameters:
```
- '-loglevel=debug'
- '-allowGET=.*'
- '-allowHEAD=.*'
- '-allowPOST=.*'
- '-allowPUT=.*'
- '-allowPATCH=.*'
- '-allowDELETE=.*'
- '-allowCONNECT=.*'
- '-allowTRACE=.*'
- '-allowOPTIONS=.*'
```

### Parameters

| Parameter | Default Value | Description |
|----------------------|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `127.0.0.1/32` | Specifies the IP addresses of the clients 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`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowhealthcheck` | (not set) | 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` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | (not set) | If set, it enables logging in JSON format. If unset, docker-proxy logs in plain text format. |
| `-loglevel` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `10` | Defines the time in seconds to wait before forcing the shutdown after sigtern or sigint (socket-proxy first tries to graceful shut down the TCP server) |
| `-socketpath` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | (not set) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `0` | Check for socket availabibity every x seconds (disable checks, if not set or value is 0) |
| Parameter | Default Value | Description |
|----------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-allowfrom` | `127.0.0.1/32` | Specifies the IP addresses 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`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. |
| `-allowhealthcheck` | (not set) | 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` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. |
| `-logjson` | (not set) | If set, it enables logging in JSON format. If unset, docker-proxy logs in plain text format. |
| `-loglevel` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. |
| `-proxyport` | `2375` | Defines the TCP port the proxy listens to. |
| `-shutdowngracetime` | `10` | Defines the time in seconds to wait before forcing the shutdown after sigtern or sigint (socket-proxy first tries to graceful shut down the TCP server) |
| `-socketpath` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. |
| `-stoponwatchdog` | (not set) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. |
| `-watchdoginterval` | `0` | Check for socket availabibity every x seconds (disable checks, if not set or value is 0) |

## License

Expand Down
34 changes: 24 additions & 10 deletions cmd/socket-proxy/handlehttprequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func handleHttpRequest(w http.ResponseWriter, r *http.Request) {

// check if the client's IP is allowed to access
allowedIP, err := isAllowedIP(r.RemoteAddr)
allowedIP, err := isAllowedClient(r.RemoteAddr)
if err != nil {
slog.Error("invalid RemoteAddr format", "reason", err, "method", r.Method, "URL", r.URL, "client", r.RemoteAddr)
sendHTTPError(w, http.StatusInternalServerError)
Expand All @@ -41,24 +41,38 @@ func handleHttpRequest(w http.ResponseWriter, r *http.Request) {
socketProxy.ServeHTTP(w, r) // proxy the request
}

// isAllowedIP checks if the given remote address is allowed to connect to the proxy.
// isAllowedClient checks if the given remote address is allowed to connect to the proxy.
// The IP address is extracted from a RemoteAddr string (the part before the colon).
func isAllowedIP(remoteAddr string) (bool, error) {
// Get the IP address from the remote address string
ipStr, _, err := net.SplitHostPort(remoteAddr)
func isAllowedClient(remoteAddr string) (bool, error) {
// Get the client IP address from the remote address string
clientIPStr, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return false, err
}
// Parse the IP address
ip := net.ParseIP(ipStr)
if ip == nil {
clientIP := net.ParseIP(clientIPStr)
if clientIP == nil {
return false, errors.New("invalid IP format")
}
// check if IP address is in allowed network
if !config.AllowedNetwork.Contains(ip) {

_, allowedIPNet, err := net.ParseCIDR(cfg.AllowFrom)
if err == nil {
// AllowFrom is a valid CIDR, so check if IP address is in allowed network
return allowedIPNet.Contains(clientIP), nil
} else {
// AllowFrom is not a valid CIDR, so try to resolve it via DNS
ips, err := net.LookupIP(cfg.AllowFrom)
if err != nil {
return false, errors.New("error looking up allowed client hostname: " + err.Error())
}
for _, ip := range ips {
// Check if IP address is one of the resolved IPs
if ip.Equal(clientIP) {
return true, nil
}
}
return false, nil
}
return true, nil
}

// sendHTTPError sends a HTTP error with the given status code.
Expand Down
28 changes: 0 additions & 28 deletions cmd/socket-proxy/handlehttprequest_test.go

This file was deleted.

2 changes: 1 addition & 1 deletion cmd/socket-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func main() {

// print configuration
slog.Info("starting socket-proxy", "version", version, "os", runtime.GOOS, "arch", runtime.GOARCH, "runtime", runtime.Version(), "URL", programUrl)
slog.Info("configuration info", "socketpath", cfg.SocketPath, "listenaddress", cfg.ListenAddress, "loglevel", cfg.LogLevel, "logjson", cfg.LogJSON, "allowfrom", config.AllowedNetwork, "shutdowngracetime", cfg.ShutdownGraceTime)
slog.Info("configuration info", "socketpath", cfg.SocketPath, "listenaddress", cfg.ListenAddress, "loglevel", cfg.LogLevel, "logjson", cfg.LogJSON, "allowfrom", cfg.AllowFrom, "shutdowngracetime", cfg.ShutdownGraceTime)
if cfg.WatchdogInterval > 0 {
slog.Info("watchdog enabled", "interval", cfg.WatchdogInterval, "stoponwatchdog", cfg.StopOnWatchdog)
} else {
Expand Down
5 changes: 1 addition & 4 deletions examples/docker-compose/dozzle/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ services:
image: wollomatic/socket-proxy:1
command:
- '-loglevel=info'
- '-allowfrom=192.168.254.8/29' # allow only the small subnet "docker-proxynet"
- '-allowfrom=dozzle' # allow only the small subnet "docker-proxynet"
- '-listenip=0.0.0.0'
- '-allowGET=/v1\..{2}/(containers/.*|events)'
- '-allowHEAD=/_ping'
Expand Down Expand Up @@ -49,9 +49,6 @@ networks:
docker-proxynet:
internal: true
attachable: false
ipam:
config:
- subnet: 192.168.254.8/29
dozzle:
driver: bridge
attachable: false
Loading
Loading