Initial release
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user