Initial release
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=egress-proxy
|
||||||
|
|
||||||
|
# Authenticated upstream HTTP proxy
|
||||||
|
UPSTREAM_HOST=strictlypragmatic.ru
|
||||||
|
UPSTREAM_PORT=8088
|
||||||
|
UPSTREAM_USERNAME=pragmatic
|
||||||
|
UPSTREAM_PASSWORD=MALAku4a0204!
|
||||||
|
|
||||||
|
# Native tarampampam/3proxy settings
|
||||||
|
LOG_OUTPUT=/dev/stdout
|
||||||
|
PROXY_PORT=3128
|
||||||
|
SOCKS_PORT=1080
|
||||||
|
PRIMARY_RESOLVER=8.8.8.8
|
||||||
|
SECONDARY_RESOLVER=8.8.8.8
|
||||||
|
DNS_CACHE_SIZE=65536
|
||||||
|
MAX_CONNECTIONS=100
|
||||||
|
NOFILE_SOFT=2048
|
||||||
|
NOFILE_HARD=4096
|
||||||
|
|
||||||
|
# 3proxy chaining and access control
|
||||||
|
PARENT_RETRIES=3
|
||||||
|
|
||||||
|
# Comma-separated source addresses/CIDRs, or * for everyone.
|
||||||
|
# Examples:
|
||||||
|
# CLIENT_ALLOW=192.168.0.0/16
|
||||||
|
# CLIENT_ALLOW=192.168.1.10,192.168.1.11
|
||||||
|
CLIENT_ALLOW=*
|
||||||
|
|
||||||
|
# Additional arguments appended to the generated listeners.
|
||||||
|
PROXY_EXTRA_ARGS=
|
||||||
|
SOCKS_EXTRA_ARGS=
|
||||||
|
|
||||||
|
# Standalone port publishing
|
||||||
|
BIND_ADDRESS=127.0.0.1
|
||||||
|
HTTP_PUBLIC_PORT=3128
|
||||||
|
SOCKS_PUBLIC_PORT=1080
|
||||||
|
|
||||||
|
# Existing Traefik integration
|
||||||
|
TRAEFIK_NETWORK=traefik
|
||||||
|
TRAEFIK_ROUTER_PREFIX=chained-3proxy
|
||||||
|
TRAEFIK_HTTP_ENTRYPOINT=proxy-http
|
||||||
|
TRAEFIK_SOCKS_ENTRYPOINT=proxy-socks
|
||||||
|
|
||||||
|
# Existing containerized Nginx/HAProxy integration
|
||||||
|
EDGE_NETWORK=edge
|
||||||
|
EDGE_SERVICE_ALIAS=chained-3proxy
|
||||||
|
|
||||||
|
# Smoke tests
|
||||||
|
TEST_IMAGE=curlimages/curl:8.14.1
|
||||||
|
|
||||||
|
# These endpoints must return only the public IP address.
|
||||||
|
TEST_HTTP_URL=http://ipinfo.io
|
||||||
|
TEST_HTTPS_URL=https://ipinfo.io
|
||||||
|
|
||||||
|
TEST_ATTEMPTS=20
|
||||||
|
TEST_RETRY_DELAY=1
|
||||||
|
TEST_TIMEOUT=15
|
||||||
|
|
||||||
|
# Strongest validation method when the upstream has a fixed exit IP.
|
||||||
|
EXPECTED_EXIT_IP=
|
||||||
|
|
||||||
|
# Disable this if the upstream rotates between several exit IPs.
|
||||||
|
ASSERT_SAME_PROXY_EXIT=false
|
||||||
|
|
||||||
|
# Disable this if direct internet access is blocked or if the upstream
|
||||||
|
# legitimately uses the same public IP as the Docker host.
|
||||||
|
ASSERT_DIFFERENT_FROM_DIRECT=true
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=egress-proxy
|
||||||
|
|
||||||
|
# Authenticated upstream HTTP proxy
|
||||||
|
UPSTREAM_HOST=
|
||||||
|
UPSTREAM_PORT=8080
|
||||||
|
UPSTREAM_USERNAME=
|
||||||
|
UPSTREAM_PASSWORD=
|
||||||
|
|
||||||
|
# Native tarampampam/3proxy settings
|
||||||
|
LOG_OUTPUT=/dev/stdout
|
||||||
|
PROXY_PORT=3128
|
||||||
|
SOCKS_PORT=1080
|
||||||
|
PRIMARY_RESOLVER=1.1.1.1
|
||||||
|
SECONDARY_RESOLVER=8.8.8.8
|
||||||
|
DNS_CACHE_SIZE=65536
|
||||||
|
MAX_CONNECTIONS=100
|
||||||
|
NOFILE_SOFT=2048
|
||||||
|
NOFILE_HARD=4096
|
||||||
|
|
||||||
|
# 3proxy chaining and access control
|
||||||
|
PARENT_RETRIES=3
|
||||||
|
|
||||||
|
# Comma-separated source addresses/CIDRs, or * for everyone.
|
||||||
|
# Examples:
|
||||||
|
# CLIENT_ALLOW=192.168.0.0/16
|
||||||
|
# CLIENT_ALLOW=192.168.1.10,192.168.1.11
|
||||||
|
CLIENT_ALLOW=*
|
||||||
|
|
||||||
|
# Additional arguments appended to the generated listeners.
|
||||||
|
PROXY_EXTRA_ARGS=
|
||||||
|
SOCKS_EXTRA_ARGS=
|
||||||
|
|
||||||
|
# Standalone port publishing
|
||||||
|
BIND_ADDRESS=127.0.0.1
|
||||||
|
HTTP_PUBLIC_PORT=3128
|
||||||
|
SOCKS_PUBLIC_PORT=1080
|
||||||
|
|
||||||
|
# Existing Traefik integration
|
||||||
|
TRAEFIK_NETWORK=traefik
|
||||||
|
TRAEFIK_ROUTER_PREFIX=chained-3proxy
|
||||||
|
TRAEFIK_HTTP_ENTRYPOINT=proxy-http
|
||||||
|
TRAEFIK_SOCKS_ENTRYPOINT=proxy-socks
|
||||||
|
|
||||||
|
# Existing containerized Nginx/HAProxy integration
|
||||||
|
EDGE_NETWORK=edge
|
||||||
|
EDGE_SERVICE_ALIAS=chained-3proxy
|
||||||
|
|
||||||
|
# Smoke tests
|
||||||
|
TEST_IMAGE=curlimages/curl:8.14.1
|
||||||
|
|
||||||
|
# These endpoints must return only the public IP address.
|
||||||
|
TEST_HTTP_URL=http://api.ipify.org
|
||||||
|
TEST_HTTPS_URL=https://api.ipify.org
|
||||||
|
|
||||||
|
TEST_ATTEMPTS=20
|
||||||
|
TEST_RETRY_DELAY=1
|
||||||
|
TEST_TIMEOUT=15
|
||||||
|
|
||||||
|
# Strongest validation method when the upstream has a fixed exit IP.
|
||||||
|
EXPECTED_EXIT_IP=
|
||||||
|
|
||||||
|
# Disable this if the upstream rotates between several exit IPs.
|
||||||
|
ASSERT_SAME_PROXY_EXIT=true
|
||||||
|
|
||||||
|
# Disable this if direct internet access is blocked or if the upstream
|
||||||
|
# legitimately uses the same public IP as the Docker host.
|
||||||
|
ASSERT_DIFFERENT_FROM_DIRECT=true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
services:
|
||||||
|
proxy:
|
||||||
|
ports:
|
||||||
|
- "${BIND_ADDRESS:-127.0.0.1}:${HTTP_PUBLIC_PORT:-3128}:${PROXY_PORT:-3128}/tcp"
|
||||||
|
- "${BIND_ADDRESS:-127.0.0.1}:${SOCKS_PUBLIC_PORT:-1080}:${SOCKS_PORT:-1080}/tcp"
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: ${TEST_IMAGE:-curlimages/curl:8.14.1}
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
environment:
|
||||||
|
PROXY_HOST: proxy
|
||||||
|
PROXY_PORT: "${PROXY_PORT:-3128}"
|
||||||
|
SOCKS_PORT: "${SOCKS_PORT:-1080}"
|
||||||
|
|
||||||
|
TEST_HTTP_URL: "${TEST_HTTP_URL:-http://api.ipify.org}"
|
||||||
|
TEST_HTTPS_URL: "${TEST_HTTPS_URL:-https://api.ipify.org}"
|
||||||
|
TEST_ATTEMPTS: "${TEST_ATTEMPTS:-20}"
|
||||||
|
TEST_RETRY_DELAY: "${TEST_RETRY_DELAY:-1}"
|
||||||
|
TEST_TIMEOUT: "${TEST_TIMEOUT:-15}"
|
||||||
|
|
||||||
|
EXPECTED_EXIT_IP: "${EXPECTED_EXIT_IP:-}"
|
||||||
|
ASSERT_SAME_PROXY_EXIT: "${ASSERT_SAME_PROXY_EXIT:-true}"
|
||||||
|
ASSERT_DIFFERENT_FROM_DIRECT: "${ASSERT_DIFFERENT_FROM_DIRECT:-true}"
|
||||||
|
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- /tests/smoke.sh
|
||||||
|
|
||||||
|
command: []
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ./tests/smoke.sh:/tests/smoke.sh:ro
|
||||||
|
|
||||||
|
read_only: true
|
||||||
|
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=1m,mode=1777
|
||||||
|
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
proxy:
|
||||||
|
environment:
|
||||||
|
# Traefik sends PROXY protocol v1 to preserve the original client IP.
|
||||||
|
#
|
||||||
|
# Existing custom listener arguments are appended after -H.
|
||||||
|
PROXY_EXTRA_ARGS: "-H ${PROXY_EXTRA_ARGS:-}"
|
||||||
|
SOCKS_EXTRA_ARGS: "-H ${SOCKS_EXTRA_ARGS:-}"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
- traefik
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=${TRAEFIK_NETWORK:-traefik}"
|
||||||
|
|
||||||
|
# HTTP proxy router and service
|
||||||
|
- "traefik.tcp.routers.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-http.entrypoints=${TRAEFIK_HTTP_ENTRYPOINT:-proxy-http}"
|
||||||
|
- "traefik.tcp.routers.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-http.rule=HostSNI(`*`)"
|
||||||
|
- "traefik.tcp.routers.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-http.service=${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-http"
|
||||||
|
- "traefik.tcp.services.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-http.loadbalancer.server.port=${PROXY_PORT:-3128}"
|
||||||
|
- "traefik.tcp.services.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-http.loadbalancer.proxyprotocol.version=1"
|
||||||
|
|
||||||
|
# SOCKS router and service
|
||||||
|
- "traefik.tcp.routers.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-socks.entrypoints=${TRAEFIK_SOCKS_ENTRYPOINT:-proxy-socks}"
|
||||||
|
- "traefik.tcp.routers.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-socks.rule=HostSNI(`*`)"
|
||||||
|
- "traefik.tcp.routers.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-socks.service=${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-socks"
|
||||||
|
- "traefik.tcp.services.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-socks.loadbalancer.server.port=${SOCKS_PORT:-1080}"
|
||||||
|
- "traefik.tcp.services.${TRAEFIK_ROUTER_PREFIX:-chained-3proxy}-socks.loadbalancer.proxyprotocol.version=1"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
name: "${TRAEFIK_NETWORK:-traefik}"
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: ${COMPOSE_PROJECT_NAME:-chained-3proxy}
|
||||||
|
|
||||||
|
services:
|
||||||
|
proxy:
|
||||||
|
image: ghcr.io/tarampampam/3proxy:2
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
environment:
|
||||||
|
LOG_OUTPUT: "${LOG_OUTPUT:-/dev/stdout}"
|
||||||
|
PROXY_PORT: "${PROXY_PORT:-3128}"
|
||||||
|
SOCKS_PORT: "${SOCKS_PORT:-1080}"
|
||||||
|
PRIMARY_RESOLVER: "${PRIMARY_RESOLVER:-1.1.1.1}"
|
||||||
|
SECONDARY_RESOLVER: "${SECONDARY_RESOLVER:-8.8.8.8}"
|
||||||
|
DNS_CACHE_SIZE: "${DNS_CACHE_SIZE:-65536}"
|
||||||
|
MAX_CONNECTIONS: "${MAX_CONNECTIONS:-100}"
|
||||||
|
|
||||||
|
PROXY_EXTRA_ARGS: "${PROXY_EXTRA_ARGS:-}"
|
||||||
|
SOCKS_EXTRA_ARGS: "${SOCKS_EXTRA_ARGS:-}"
|
||||||
|
|
||||||
|
# EXTRA_CONFIG is inserted before both generated listeners.
|
||||||
|
#
|
||||||
|
# Operation-specific ACLs select the correct parent type:
|
||||||
|
#
|
||||||
|
# - HTTP/HTTPS operations belong to the HTTP proxy listener.
|
||||||
|
# - CONNECT belongs to the SOCKS listener.
|
||||||
|
#
|
||||||
|
# auth iponly does not require client credentials, but enables
|
||||||
|
# ACL and parent routing.
|
||||||
|
EXTRA_CONFIG: |
|
||||||
|
parentretries ${PARENT_RETRIES:-3}
|
||||||
|
auth iponly
|
||||||
|
|
||||||
|
allow * ${CLIENT_ALLOW:-*} * * HTTP,HTTPS
|
||||||
|
parent 1000 http "${UPSTREAM_HOST:?Set UPSTREAM_HOST in .env}" ${UPSTREAM_PORT:-8080} "${UPSTREAM_USERNAME:?Set UPSTREAM_USERNAME in .env}" "${UPSTREAM_PASSWORD:?Set UPSTREAM_PASSWORD in .env}"
|
||||||
|
|
||||||
|
allow * ${CLIENT_ALLOW:-*} * * CONNECT
|
||||||
|
parent 1000 connect "${UPSTREAM_HOST:?Set UPSTREAM_HOST in .env}" ${UPSTREAM_PORT:-8080} "${UPSTREAM_USERNAME:?Set UPSTREAM_USERNAME in .env}" "${UPSTREAM_PASSWORD:?Set UPSTREAM_PASSWORD in .env}"
|
||||||
|
|
||||||
|
deny *
|
||||||
|
|
||||||
|
expose:
|
||||||
|
- "${PROXY_PORT:-3128}"
|
||||||
|
- "${SOCKS_PORT:-1080}"
|
||||||
|
|
||||||
|
read_only: true
|
||||||
|
|
||||||
|
tmpfs:
|
||||||
|
# The native image entrypoint generates /etc/3proxy/3proxy.cfg.
|
||||||
|
- /etc/3proxy:size=1m,mode=0755,uid=10001,gid=10001
|
||||||
|
- /tmp:size=1m,mode=1777
|
||||||
|
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: ${NOFILE_SOFT:-2048}
|
||||||
|
hard: ${NOFILE_HARD:-4096}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
docker compose \
|
||||||
|
-f compose.yaml \
|
||||||
|
-f compose.test.yaml \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
compose down --remove-orphans
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
compose up \
|
||||||
|
--force-recreate \
|
||||||
|
--abort-on-container-exit \
|
||||||
|
--exit-code-from test
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PROXY_HOST="${PROXY_HOST:-proxy}"
|
||||||
|
PROXY_PORT="${PROXY_PORT:-3128}"
|
||||||
|
SOCKS_PORT="${SOCKS_PORT:-1080}"
|
||||||
|
|
||||||
|
TEST_HTTP_URL="${TEST_HTTP_URL:-http://api.ipify.org}"
|
||||||
|
TEST_HTTPS_URL="${TEST_HTTPS_URL:-https://api.ipify.org}"
|
||||||
|
TEST_ATTEMPTS="${TEST_ATTEMPTS:-20}"
|
||||||
|
TEST_RETRY_DELAY="${TEST_RETRY_DELAY:-1}"
|
||||||
|
TEST_TIMEOUT="${TEST_TIMEOUT:-15}"
|
||||||
|
|
||||||
|
EXPECTED_EXIT_IP="${EXPECTED_EXIT_IP:-}"
|
||||||
|
ASSERT_SAME_PROXY_EXIT="${ASSERT_SAME_PROXY_EXIT:-true}"
|
||||||
|
ASSERT_DIFFERENT_FROM_DIRECT="${ASSERT_DIFFERENT_FROM_DIRECT:-true}"
|
||||||
|
|
||||||
|
normalize_response() {
|
||||||
|
printf '%s' "$1" | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
request() {
|
||||||
|
label="$1"
|
||||||
|
proxy_url="$2"
|
||||||
|
target_url="$3"
|
||||||
|
attempt=1
|
||||||
|
|
||||||
|
while [ "$attempt" -le "$TEST_ATTEMPTS" ]; do
|
||||||
|
printf 'Testing %s, attempt %s/%s...\n' \
|
||||||
|
"$label" \
|
||||||
|
"$attempt" \
|
||||||
|
"$TEST_ATTEMPTS" >&2
|
||||||
|
|
||||||
|
if [ -n "$proxy_url" ]; then
|
||||||
|
if response="$(
|
||||||
|
curl -q \
|
||||||
|
--fail \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--location \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time "$TEST_TIMEOUT" \
|
||||||
|
--noproxy "" \
|
||||||
|
--proxy "$proxy_url" \
|
||||||
|
"$target_url" \
|
||||||
|
2>/tmp/curl-error
|
||||||
|
)"; then
|
||||||
|
response="$(normalize_response "$response")"
|
||||||
|
|
||||||
|
if [ -n "$response" ]; then
|
||||||
|
printf '%s' "$response"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if response="$(
|
||||||
|
curl -q \
|
||||||
|
--fail \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--location \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time "$TEST_TIMEOUT" \
|
||||||
|
--noproxy "*" \
|
||||||
|
"$target_url" \
|
||||||
|
2>/tmp/curl-error
|
||||||
|
)"; then
|
||||||
|
response="$(normalize_response "$response")"
|
||||||
|
|
||||||
|
if [ -n "$response" ]; then
|
||||||
|
printf '%s' "$response"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
|
||||||
|
if [ "$attempt" -le "$TEST_ATTEMPTS" ]; then
|
||||||
|
sleep "$TEST_RETRY_DELAY"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s failed after %s attempts.\n' \
|
||||||
|
"$label" \
|
||||||
|
"$TEST_ATTEMPTS" >&2
|
||||||
|
|
||||||
|
if [ -s /tmp/curl-error ]; then
|
||||||
|
cat /tmp/curl-error >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_expected_ip() {
|
||||||
|
label="$1"
|
||||||
|
actual="$2"
|
||||||
|
|
||||||
|
if [ "$actual" != "$EXPECTED_EXIT_IP" ]; then
|
||||||
|
printf 'FAIL: %s used an unexpected exit IP.\n' "$label" >&2
|
||||||
|
printf 'Expected: %s\n' "$EXPECTED_EXIT_IP" >&2
|
||||||
|
printf 'Actual: %s\n' "$actual" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
printf '%s\n' 'Starting 3proxy smoke tests...'
|
||||||
|
|
||||||
|
http_ip="$(
|
||||||
|
request \
|
||||||
|
'HTTP listener with an HTTP target' \
|
||||||
|
"http://${PROXY_HOST}:${PROXY_PORT}" \
|
||||||
|
"$TEST_HTTP_URL"
|
||||||
|
)"
|
||||||
|
|
||||||
|
https_ip="$(
|
||||||
|
request \
|
||||||
|
'HTTP listener with an HTTPS target' \
|
||||||
|
"http://${PROXY_HOST}:${PROXY_PORT}" \
|
||||||
|
"$TEST_HTTPS_URL"
|
||||||
|
)"
|
||||||
|
|
||||||
|
socks_ip="$(
|
||||||
|
request \
|
||||||
|
'SOCKS listener with remote DNS' \
|
||||||
|
"socks5h://${PROXY_HOST}:${SOCKS_PORT}" \
|
||||||
|
"$TEST_HTTPS_URL"
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf 'HTTP target exit IP: %s\n' "$http_ip"
|
||||||
|
printf 'HTTPS target exit IP: %s\n' "$https_ip"
|
||||||
|
printf 'SOCKS exit IP: %s\n' "$socks_ip"
|
||||||
|
|
||||||
|
if [ -n "$EXPECTED_EXIT_IP" ]; then
|
||||||
|
assert_expected_ip 'HTTP target' "$http_ip"
|
||||||
|
assert_expected_ip 'HTTPS target' "$https_ip"
|
||||||
|
assert_expected_ip 'SOCKS target' "$socks_ip"
|
||||||
|
elif [ "$ASSERT_SAME_PROXY_EXIT" = 'true' ]; then
|
||||||
|
if [ "$http_ip" != "$https_ip" ] || [ "$http_ip" != "$socks_ip" ]; then
|
||||||
|
printf '%s\n' \
|
||||||
|
'FAIL: proxy listeners returned different exit IPs.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ASSERT_DIFFERENT_FROM_DIRECT" = 'true' ]; then
|
||||||
|
direct_ip="$(request 'direct connection' '' "$TEST_HTTPS_URL")"
|
||||||
|
|
||||||
|
printf 'Direct exit IP: %s\n' "$direct_ip"
|
||||||
|
|
||||||
|
if [ "$http_ip" = "$direct_ip" ] || \
|
||||||
|
[ "$https_ip" = "$direct_ip" ] || \
|
||||||
|
[ "$socks_ip" = "$direct_ip" ]; then
|
||||||
|
printf '%s\n' \
|
||||||
|
'FAIL: at least one proxied request used the direct exit IP.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'PASS: all proxy smoke tests succeeded.'
|
||||||
Reference in New Issue
Block a user