595 lines
13 KiB
Markdown
595 lines
13 KiB
Markdown
# Egress Proxy
|
|
|
|
> [!WARNING]
|
|
> This project was created with the assistance of an AI agent.
|
|
>
|
|
> Although it has been tested in production, it is provided as-is. I do not accept responsibility for configuration errors, service interruptions, exposed credentials, open proxy abuse, or security vulnerabilities.
|
|
|
|
A Docker Compose wrapper around [tarampampam/3proxy-docker](https://github.com/tarampampam/3proxy-docker) designed to expose local HTTP and SOCKS proxy endpoints and route their traffic through an authenticated upstream HTTP proxy.
|
|
|
|
The stack preserves the dynamic configuration provided by the original image and adds optional deployment integrations for standalone Docker, Traefik, Nginx, HAProxy, and other TCP reverse proxies.
|
|
|
|
## Requirements
|
|
|
|
* Docker Engine
|
|
* Docker Compose v2
|
|
* An upstream HTTP proxy
|
|
* Upstream proxy hostname or IP address
|
|
* Upstream proxy port
|
|
* Upstream proxy username and password
|
|
* Optional external Docker network when integrating with Traefik, Nginx, or HAProxy
|
|
|
|
The upstream proxy must support the HTTP `CONNECT` method for HTTPS and SOCKS TCP forwarding.
|
|
|
|
## Features
|
|
|
|
* HTTP forward proxy on port `3128`
|
|
* SOCKS proxy on port `1080`
|
|
* Routes supported traffic through an authenticated upstream HTTP proxy
|
|
* No authentication required from local proxy clients
|
|
* Dynamic configuration through environment variables
|
|
* Preserves native `3proxy-docker` configuration options
|
|
* Does not replace the original image entrypoint
|
|
* Does not mount a complete custom `3proxy.cfg`
|
|
* Uses the image's native `EXTRA_CONFIG` mechanism
|
|
* Standalone deployment with directly published ports
|
|
* Traefik TCP router integration
|
|
* Nginx and HAProxy integration through a shared Docker network
|
|
* Optional PROXY protocol support with Traefik
|
|
* Configurable client IP allowlist
|
|
* Configurable DNS resolvers, connection limits, ports, and listener arguments
|
|
* Automatic restart through Docker restart policies
|
|
* Smoke-test suite for HTTP, HTTPS, and SOCKS forwarding
|
|
* Optional verification of the expected upstream exit IP
|
|
|
|
## Repository Structure
|
|
|
|
```text
|
|
.
|
|
├── .env.example
|
|
├── .gitattributes
|
|
├── .gitignore
|
|
├── README.md
|
|
├── compose.yaml
|
|
├── compose.standalone.yaml
|
|
├── compose.traefik.yaml
|
|
├── compose.external-network.yaml
|
|
├── compose.test.yaml
|
|
└── tests/
|
|
├── run.sh
|
|
└── smoke.sh
|
|
```
|
|
|
|
## How It Works
|
|
|
|
The base image generates the normal 3proxy configuration using its built-in environment-variable support.
|
|
|
|
This project adds parent routing rules through `EXTRA_CONFIG`.
|
|
|
|
The generated configuration is equivalent to:
|
|
|
|
```text
|
|
parentretries 3
|
|
auth iponly
|
|
|
|
allow * * * * HTTP,HTTPS
|
|
parent 1000 http "proxy.example.com" 8080 "username" "password"
|
|
|
|
allow * * * * CONNECT
|
|
parent 1000 connect "proxy.example.com" 8080 "username" "password"
|
|
|
|
deny *
|
|
```
|
|
|
|
The operation-specific access rules are important:
|
|
|
|
* `HTTP,HTTPS` handles requests received by the HTTP proxy listener.
|
|
* `CONNECT` handles TCP connections received by the SOCKS listener.
|
|
* `http` is used as the upstream parent type for the HTTP listener.
|
|
* `connect` is used to tunnel SOCKS TCP traffic through the upstream HTTP proxy.
|
|
* SOCKS UDP association and bind operations are not supported by an upstream HTTP proxy.
|
|
|
|
The stack uses:
|
|
|
|
```text
|
|
auth iponly
|
|
```
|
|
|
|
This does not require clients to provide a username or password. It enables 3proxy ACL and parent-routing processing.
|
|
|
|
Using:
|
|
|
|
```text
|
|
auth none
|
|
```
|
|
|
|
would disable the ACL chain and may cause requests to bypass the upstream proxy.
|
|
|
|
## Configuration
|
|
|
|
Copy the example environment file:
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
At minimum, configure the upstream proxy:
|
|
|
|
```dotenv
|
|
UPSTREAM_HOST=proxy.example.com
|
|
UPSTREAM_PORT=8080
|
|
UPSTREAM_USERNAME=proxy-user
|
|
UPSTREAM_PASSWORD='proxy-password'
|
|
```
|
|
|
|
The `.env` file is ignored by Git and must not be committed.
|
|
|
|
### Credentials with Special Characters
|
|
|
|
Use single quotes for values containing `$`, `#`, spaces, or similar characters:
|
|
|
|
```dotenv
|
|
UPSTREAM_PASSWORD='my$p@ssword#123'
|
|
```
|
|
|
|
Avoid double quotes and newline characters inside upstream credentials because the values are inserted into quoted 3proxy configuration arguments.
|
|
|
|
## Available Environment Variables
|
|
|
|
### Image and Project
|
|
|
|
```dotenv
|
|
COMPOSE_PROJECT_NAME=egress-proxy
|
|
THREEPROXY_IMAGE=ghcr.io/tarampampam/3proxy:2
|
|
```
|
|
|
|
### Upstream Proxy
|
|
|
|
```dotenv
|
|
UPSTREAM_HOST=proxy.example.com
|
|
UPSTREAM_PORT=8080
|
|
UPSTREAM_USERNAME=proxy-user
|
|
UPSTREAM_PASSWORD='proxy-password'
|
|
```
|
|
|
|
### Local Proxy Listeners
|
|
|
|
```dotenv
|
|
PROXY_PORT=3128
|
|
SOCKS_PORT=1080
|
|
```
|
|
|
|
### DNS
|
|
|
|
```dotenv
|
|
PRIMARY_RESOLVER=1.1.1.1
|
|
SECONDARY_RESOLVER=8.8.8.8
|
|
DNS_CACHE_SIZE=65536
|
|
```
|
|
|
|
### Limits
|
|
|
|
```dotenv
|
|
MAX_CONNECTIONS=100
|
|
PARENT_RETRIES=3
|
|
NOFILE_SOFT=2048
|
|
NOFILE_HARD=4096
|
|
```
|
|
|
|
### Client Access
|
|
|
|
Allow all clients:
|
|
|
|
```dotenv
|
|
CLIENT_ALLOW=*
|
|
```
|
|
|
|
Allow a private network:
|
|
|
|
```dotenv
|
|
CLIENT_ALLOW=192.168.0.0/16
|
|
```
|
|
|
|
Allow several addresses or networks:
|
|
|
|
```dotenv
|
|
CLIENT_ALLOW=192.168.0.0/16,203.0.113.10
|
|
```
|
|
|
|
### Listener Arguments
|
|
|
|
Additional arguments can be appended to the generated listeners:
|
|
|
|
```dotenv
|
|
PROXY_EXTRA_ARGS=
|
|
SOCKS_EXTRA_ARGS=
|
|
```
|
|
|
|
These values preserve the customization options provided by the original `3proxy-docker` image.
|
|
|
|
## Validate the Configuration
|
|
|
|
Before starting the stack, validate the merged Compose configuration.
|
|
|
|
Standalone deployment:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.standalone.yaml \
|
|
config
|
|
```
|
|
|
|
Traefik deployment:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.traefik.yaml \
|
|
config
|
|
```
|
|
|
|
External network deployment:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.external-network.yaml \
|
|
config
|
|
```
|
|
|
|
Test deployment:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.test.yaml \
|
|
config
|
|
```
|
|
|
|
## Standalone Deployment
|
|
|
|
Use the standalone overlay when there is no existing reverse proxy.
|
|
|
|
By default, the proxy binds only to localhost:
|
|
|
|
```dotenv
|
|
BIND_ADDRESS=127.0.0.1
|
|
HTTP_PUBLIC_PORT=3128
|
|
SOCKS_PUBLIC_PORT=1080
|
|
```
|
|
|
|
Start the stack:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.standalone.yaml \
|
|
up -d
|
|
```
|
|
|
|
Test the HTTP proxy:
|
|
|
|
```bash
|
|
curl \
|
|
--proxy http://127.0.0.1:3128 \
|
|
https://api.ipify.org
|
|
```
|
|
|
|
Test the SOCKS proxy:
|
|
|
|
```bash
|
|
curl \
|
|
--proxy socks5h://127.0.0.1:1080 \
|
|
https://api.ipify.org
|
|
```
|
|
|
|
View logs:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.standalone.yaml \
|
|
logs -f proxy
|
|
```
|
|
|
|
Stop the stack:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.standalone.yaml \
|
|
down
|
|
```
|
|
|
|
### LAN Access
|
|
|
|
To expose the proxy on all server interfaces:
|
|
|
|
```dotenv
|
|
BIND_ADDRESS=0.0.0.0
|
|
```
|
|
|
|
Restrict clients when exposing the service outside localhost:
|
|
|
|
```dotenv
|
|
CLIENT_ALLOW=192.168.0.0/16
|
|
```
|
|
|
|
Do not expose an unauthenticated proxy to the public internet without an IP allowlist, firewall rules, VPN, private network, or equivalent access control.
|
|
|
|
## Traefik Deployment
|
|
|
|
3proxy must be exposed through Traefik TCP routers, not normal HTTP routers.
|
|
|
|
The HTTP proxy and SOCKS listener are separate TCP services and require dedicated Traefik entrypoints.
|
|
|
|
Add the following static configuration to the existing Traefik service:
|
|
|
|
```yaml
|
|
services:
|
|
traefik:
|
|
command:
|
|
- --entrypoints.proxy-http.address=:3128/tcp
|
|
- --entrypoints.proxy-socks.address=:1080/tcp
|
|
|
|
ports:
|
|
- "3128:3128/tcp"
|
|
- "1080:1080/tcp"
|
|
```
|
|
|
|
Create the shared network if it does not already exist:
|
|
|
|
```bash
|
|
docker network create traefik
|
|
```
|
|
|
|
Configure the integration:
|
|
|
|
```dotenv
|
|
TRAEFIK_NETWORK=traefik
|
|
TRAEFIK_ROUTER_PREFIX=egress-proxy
|
|
TRAEFIK_HTTP_ENTRYPOINT=proxy-http
|
|
TRAEFIK_SOCKS_ENTRYPOINT=proxy-socks
|
|
```
|
|
|
|
Start the stack:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.traefik.yaml \
|
|
up -d
|
|
```
|
|
|
|
The Traefik overlay:
|
|
|
|
* Creates one TCP router and service for the HTTP proxy.
|
|
* Creates one TCP router and service for SOCKS.
|
|
* Uses `HostSNI(*)` as a catch-all TCP routing rule.
|
|
* Sends PROXY protocol v1 to 3proxy.
|
|
* Enables `-H` on both 3proxy listeners.
|
|
* Preserves the original client IP.
|
|
* Does not publish ports directly from the 3proxy container.
|
|
|
|
Do not combine `compose.standalone.yaml` and `compose.traefik.yaml`.
|
|
|
|
In Traefik mode, Traefik should be the only service publishing ports `3128` and `1080`.
|
|
|
|
## Existing Nginx or HAProxy
|
|
|
|
Use the external-network overlay when an existing containerized TCP proxy should connect to 3proxy through Docker networking.
|
|
|
|
Create the external network:
|
|
|
|
```bash
|
|
docker network create edge
|
|
```
|
|
|
|
Configure it:
|
|
|
|
```dotenv
|
|
EDGE_NETWORK=edge
|
|
EDGE_SERVICE_ALIAS=egress-proxy
|
|
```
|
|
|
|
Start the stack:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.external-network.yaml \
|
|
up -d
|
|
```
|
|
|
|
Other containers attached to the same network can reach:
|
|
|
|
```text
|
|
egress-proxy:3128
|
|
egress-proxy:1080
|
|
```
|
|
|
|
### Nginx Example
|
|
|
|
Nginx must use the `stream` context rather than the regular `http` context:
|
|
|
|
```nginx
|
|
stream {
|
|
upstream egress_http_proxy {
|
|
server egress-proxy:3128;
|
|
}
|
|
|
|
upstream egress_socks_proxy {
|
|
server egress-proxy:1080;
|
|
}
|
|
|
|
server {
|
|
listen 3128;
|
|
proxy_pass egress_http_proxy;
|
|
}
|
|
|
|
server {
|
|
listen 1080;
|
|
proxy_pass egress_socks_proxy;
|
|
}
|
|
}
|
|
```
|
|
|
|
Without PROXY protocol, 3proxy sees the Nginx container address instead of the original client address.
|
|
|
|
In that case, client access restrictions should be applied in Nginx, or PROXY protocol should be configured on both sides.
|
|
|
|
## Smoke Tests
|
|
|
|
The test suite starts the proxy on an internal Docker network and verifies:
|
|
|
|
1. An HTTP request through the HTTP proxy listener.
|
|
2. An HTTPS request through the HTTP proxy listener.
|
|
3. An HTTPS request through the SOCKS listener.
|
|
4. That the requests return a public exit IP.
|
|
5. Optionally, that all listeners use the expected upstream exit IP.
|
|
6. Optionally, that proxied traffic does not use the server's direct public IP.
|
|
|
|
Run the tests:
|
|
|
|
```bash
|
|
sh tests/run.sh
|
|
```
|
|
|
|
The command exits with:
|
|
|
|
* `0` when all checks pass.
|
|
* A non-zero code when a request or assertion fails.
|
|
|
|
The test stack is removed automatically after completion.
|
|
|
|
### Fixed Exit IP
|
|
|
|
When the upstream proxy has a fixed public IP, configure:
|
|
|
|
```dotenv
|
|
EXPECTED_EXIT_IP=203.0.113.20
|
|
```
|
|
|
|
This is the strongest validation method.
|
|
|
|
All HTTP, HTTPS, and SOCKS requests must return that exact IP.
|
|
|
|
### Rotating Exit IPs
|
|
|
|
When the upstream proxy uses rotating IP addresses:
|
|
|
|
```dotenv
|
|
EXPECTED_EXIT_IP=
|
|
ASSERT_SAME_PROXY_EXIT=false
|
|
ASSERT_DIFFERENT_FROM_DIRECT=true
|
|
```
|
|
|
|
The test verifies that each request succeeds and does not use the server's direct public IP.
|
|
|
|
### No Direct Internet Access
|
|
|
|
When the test container cannot access the internet without the proxy:
|
|
|
|
```dotenv
|
|
ASSERT_DIFFERENT_FROM_DIRECT=false
|
|
```
|
|
|
|
Set `EXPECTED_EXIT_IP` when possible.
|
|
|
|
## Applying Configuration Changes
|
|
|
|
After modifying `.env`, recreate the container.
|
|
|
|
Standalone:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.standalone.yaml \
|
|
up -d --force-recreate
|
|
```
|
|
|
|
Traefik:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.traefik.yaml \
|
|
up -d --force-recreate
|
|
```
|
|
|
|
External network:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.external-network.yaml \
|
|
up -d --force-recreate
|
|
```
|
|
|
|
## Diagnostics
|
|
|
|
View logs:
|
|
|
|
```bash
|
|
docker compose logs -f proxy
|
|
```
|
|
|
|
View service status:
|
|
|
|
```bash
|
|
docker compose ps
|
|
```
|
|
|
|
Inspect the final merged configuration:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f compose.yaml \
|
|
-f compose.standalone.yaml \
|
|
config
|
|
```
|
|
|
|
Inspect the generated 3proxy configuration:
|
|
|
|
```bash
|
|
docker compose cp \
|
|
proxy:/etc/3proxy/3proxy.cfg \
|
|
./generated-3proxy.cfg
|
|
```
|
|
|
|
The generated configuration contains upstream credentials. Delete the copied file after debugging:
|
|
|
|
```bash
|
|
rm -f generated-3proxy.cfg
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
* Local clients are not required to authenticate.
|
|
* Restrict access using `CLIENT_ALLOW`, firewall rules, a VPN, or a private network.
|
|
* Do not expose an unrestricted proxy to the public internet.
|
|
* Upstream credentials are stored in `.env`.
|
|
* Docker administrators can inspect container environment variables.
|
|
* Never commit `.env`.
|
|
* Rotate credentials accidentally committed, logged, or shared publicly.
|
|
* Review generated Compose and 3proxy configuration before deployment.
|
|
* Pin the container image to a specific version when reproducible deployments are required.
|
|
* Keep Docker, Docker Compose, Traefik, Nginx, and the base image updated.
|
|
* Treat access to the Docker socket as administrative access to the host.
|
|
|
|
## Limitations
|
|
|
|
* The upstream must be an HTTP proxy.
|
|
* SOCKS UDP forwarding is not supported.
|
|
* SOCKS bind operations are not supported.
|
|
* Client authentication is not enabled.
|
|
* Reverse proxies must support raw TCP forwarding.
|
|
* Normal hostname-based HTTP routing is not applicable to SOCKS traffic.
|
|
* Hostname-based routing is generally not useful for a forward proxy because destination hostnames belong to outbound requests rather than the proxy service itself.
|
|
|
|
## License
|
|
|
|
Use, modify, and distribute this project under the terms of the repository license.
|