A high-performance SOCKS5 front proxy with load balancing, active health checking, and dead-state management. Lancer sits between your applications and a pool of upstream proxies, distributing traffic intelligently while continuously monitoring upstream health.
- Multi-Protocol Upstream Support — SOCKS4, SOCKS5, HTTP CONNECT, HTTPS CONNECT
- Tiered P2C Load Balancing — Latency-biased Power of Two Choices with dynamic tuning
- Active Health Checking — HTTP probe through upstreams with configurable intervals
- Dead-State Management — Automatic quarantine, periodic revival, and permanent removal
- SO_REUSEPORT Sharding — Kernel-level connection distribution across CPU cores
- Zero-Copy Relay — Linux
splice(2)for kernel-to-kernel TCP forwarding - Lock-Free Metrics — All hot-path metrics use atomic operations (no mutexes)
- Standalone Proxy Checker —
lancer pingworks independently without any config file - UDP ASSOCIATE — Full SOCKS5 UDP relay support (RFC 1928 §7)
go build -o lancer ./cmd/lancerCreate a config.json (see Configuration Reference below) and a proxy.txt file with your upstream proxies:
# proxy.txt — one proxy URI per line
socks5://127.0.0.1:1080
socks5://admin:password@10.0.0.1:1080
socks4://10.0.0.2:1080
http://proxy.example.com:8080
https://user:pass@secure-proxy.example.com:443
# Start the proxy server (default: reads config.json)
./lancer
# With a custom config path
./lancer --config /path/to/config.jsonThe ping command is fully standalone — it does not require config.json. All options are passed as CLI flags.
# Basic usage — probe all proxies in proxy.txt
./lancer ping --file proxy.txt
# Save alive proxies to a file
./lancer ping --file proxy.txt --output healthy.txt
# Custom timeout and probe URL
./lancer ping --file proxy.txt --output healthy.txt --timeout 10s
# Custom probe URL and expected status
./lancer ping --file proxy.txt --url https://example.com --status 200
# Full example with all flags
./lancer ping \
--file proxy.txt \
--output healthy.txt \
--timeout 5s \
--url https://google.com/generate_204 \
--status 204 \
--concurrency 200Ping Flags:
| Flag | Default | Description |
|---|---|---|
--file |
proxy.txt |
Input file with proxy URIs (one per line) |
--output |
(stdout only) | Write alive proxy URIs to this file |
--timeout |
5s |
Per-probe dial + HTTP timeout |
--url |
https://google.com/generate_204 |
URL to probe through each upstream |
--status |
204 |
Expected HTTP status code |
--concurrency |
200 |
Maximum concurrent probes |
The upstream file (proxy.txt by default) contains one proxy URI per line. Blank lines and lines starting with # are ignored.
Supported URI formats:
# SOCKS5 (default port: 1080)
socks5://host:port
socks5://username:password@host:port
# SOCKS4 / SOCKS4a (default port: 1080)
socks4://host:port
socks4://username@host:port
# HTTP CONNECT (default port: 8080)
http://host:port
http://username:password@host:port
# HTTPS CONNECT — TLS to proxy, then HTTP CONNECT (default port: 8080)
https://host:port
https://username:password@host:port
Protocol Details:
| Protocol | Auth | UDP ASSOCIATE | Notes |
|---|---|---|---|
| SOCKS5 | Username/Password (RFC 1929) | ✅ Yes | Full RFC 1928 compliance |
| SOCKS4 | USERID field | ❌ No | SOCKS4a domain support included |
| HTTP | Basic Auth | ❌ No | Standard HTTP CONNECT tunneling |
| HTTPS | Basic Auth | ❌ No | TLS handshake to proxy, then CONNECT |
Lancer reads its runtime configuration from config.json (or a path specified by --config). All fields have sensible defaults — an empty {} is a valid config.
Client ──SOCKS5──▶ Lancer ──[protocol]──▶ Upstream Proxy ──▶ Target
│
├── Tiered P2C Balancer (upstream selection)
├── Health Checker (periodic probing)
├── Dead State Manager (quarantine & revival)
└── Connection Manager (global limit enforcement)
cmd/lancer/
├── main.go # Entry point, server lifecycle, connection handler
└── ping.go # Standalone proxy checker (no config dependency)
pkg/
├── balancer/ # P2C and Tiered P2C selection algorithms
│ ├── p2c.go # Basic Power of Two Choices
│ ├── tiered_p2c.go # Latency-biased tiered variant with dynamic tuning
│ └── percentile.go # Lock-free ring buffer for latency percentiles
├── config/ # Configuration loading and upstream parsing
│ ├── config.go # JSON config structs, defaults, file loading
│ └── upstream.go # URI parsing (socks4/5, http/s)
├── connmgr/ # Atomic global connection limiter
├── deadfile/ # Persistent dead upstream archive (JSON)
├── dialer/ # Multi-protocol upstream dialer
│ └── dialer.go # SOCKS4, SOCKS5, HTTP CONNECT, HTTPS CONNECT
├── health/ # Active health checking
│ ├── checker.go # Periodic probe loop (HTTP or TCP)
│ └── reviver.go # Dead upstream recovery
├── relay/ # Bidirectional data relay with splice(2)
├── shard/ # SO_REUSEPORT listener sharding
├── socks5/ # SOCKS5 server protocol (RFC 1928)
│ ├── protocol.go # Handshake, request parsing, reply encoding
│ └── server.go # Accept loop and connection handler
├── udprelay/ # SOCKS5 UDP ASSOCIATE relay (RFC 1928 §7)
└── upstream/ # Upstream state management
├── upstream.go # Lock-free atomic metrics (EWMA, conns, rewards)
└── pool.go # Atomic healthy/dead pool with zero-alloc reads
The basic P2C algorithm randomly samples two upstreams and picks the one with the higher score:
score = successRate × weight × (1 / latency) × (1 / (1 + activeConns × loadPenalty))
The tiered variant adds three improvements:
-
Tier Bucketing — Upstreams are grouped into latency tiers (Fast < 50ms, Medium 50–200ms, Slow > 200ms) based on EWMA latency.
-
Weighted Tier Selection — A tier is chosen via weighted random (default: 70% Fast, 20% Medium, 10% Slow), then P2C runs within the selected tier.
-
Non-Linear Scoring — Exponents amplify differences:
score = sr^srExp × weight × reward × (1/lat^latExp) × (1/(1+conns)^loadExp) -
Dynamic Tuning — A background goroutine adapts the latency exponent based on the p90/p50 spread, and optionally recomputes tier boundaries from percentile data.
-
Exploration — A small fraction of picks (
explore_ratio) bypass tier selection entirely to gather latency data from under-sampled upstreams. -
Reinforcement Rewards — Successful relays increase an upstream's reward factor; failures decrease it (clamped to
[0.01, 2.0]).
Every probe.interval seconds, the health checker probes all healthy upstreams concurrently:
- HTTP Probe (default) — Dials through each upstream to
probe.ping.url, performs an HTTP GET, and checks the response status code. - TCP Probe (fallback) — If no ping URL is configured, performs a simple TCP dial to the upstream.
HEALTHY ──[max_fails consecutive failures]──▶ DEAD
▲ │
│ ▼
└──[TCP dial succeeds]── REVIVAL ◀── [revive_interval_seconds]
│
▼
[remove_after_cycles failures]
│
▼
PERMANENTLY REMOVED
(archived to dead.json)
- Upstreams move from HEALTHY → DEAD after
max_failsconsecutive probe failures. - Every
revive_interval_seconds, dead upstreams get a TCP dial check. - If the dial succeeds, the upstream is revived with a slow-start weight (0.1 → 1.0).
- If revival fails
remove_after_cyclestimes, the upstream is permanently removed and archived todead.json.
On Linux, when both client and upstream connections are raw TCP (*net.TCPConn), Go's ReadFrom automatically invokes splice(2) for kernel-to-kernel data transfer — data never enters userspace. The bufferedConn wrapper preserves splice eligibility by implementing io.ReaderFrom and io.WriterTo.
When tuning.enable_sharding is true, Lancer creates one listener per CPU core, all bound to the same address via SO_REUSEPORT. The kernel distributes incoming connections across listeners, eliminating accept contention.
All per-upstream metrics (EWMA latency, active connections, success/failure counters, rewards) use sync/atomic CAS loops. The healthy upstream slice is stored in an atomic.Value for zero-allocation reads on every Pick() call. Mutexes are only used for structural mutations (add/remove/mark-dead), which are infrequent.
sync.Poolfor relay buffers (configurable size, default 32KB)sync.Poolforbufio.Readerin dialer and health checker- Stack-allocated arrays for protocol negotiation (SOCKS4/5 handshakes, replies)
{
"balancer": {"algorithm": "p2c"},
"probe": {"interval": 10, "concurrency": 50},
"tuning": {"enable_sharding": false}
}{
"balancer": {
"algorithm": "tiered_p2c",
"tiered": {
"dynamic": {"enabled": true, "auto_tiers": true}
}
},
"probe": {"interval": 30, "concurrency": 200},
"connection": {"buffer_size": 65536},
"tuning": {
"max_connections": 100000,
"enable_sharding": true,
"shard_per_cpu": true
}
}| Parameter | Effect | Guidance |
|---|---|---|
probe.interval |
How often upstreams are checked | Lower = faster failover, more probe traffic |
probe.ping.max_fails |
Failures before marking dead | Lower = aggressive, Higher = tolerant |
balancer.tiered.latency_exp |
Fast upstream preference strength | 1.0 = linear, 4.0 = heavily favors fast |
balancer.tiered.dynamic.explore_ratio |
Random exploration fraction | 0.02 = 2% of picks are random exploration |
connection.buffer_size |
Relay buffer (non-splice fallback) | 32KB–64KB typical; larger for high-throughput |
tuning.max_connections |
Global connection hard limit | Match your ulimit -n |
retry.attempts |
Retries on upstream dial failure | 2–4 typical |
retry.jitter_ms |
Random delay cap between retries | Prevents thundering herd |
MIT
{ // --- Server Listener --- "listen": { "address": "0.0.0.0", // Bind address (default: "0.0.0.0") "port": 4528 // Bind port (default: 1080) }, // --- Upstream Proxy Source --- "upstream": { "file": "proxy.txt" // Path to the proxy list file (default: "proxy.txt") }, // --- Health Probing --- "probe": { "interval": 30, // Seconds between health check rounds (default: 10) "timeout": 5, // Per-probe dial timeout in seconds (default: 3) "concurrency": 200, // Max concurrent health checks (default: 100) "ping": { "url": "https://google.com/generate_204", // Probe URL (default: same) "status": 204, // Expected HTTP status (default: 204) "max_fails": 10 // Consecutive failures before marking dead (default: 3) }, "dead_state": { "revive_interval_seconds": 15, // Seconds between revival attempts (default: 900) "remove_after_cycles": 5 // Failed revival cycles before permanent removal (default: 2) } }, // --- Load Balancer --- "balancer": { "algorithm": "tiered_p2c", // "p2c" or "tiered_p2c" (default: "p2c") "metrics": { "ewma_alpha": 0.1, // EWMA smoothing factor for latency (default: 0.2) "load_penalty": 0.5 // Active connection penalty weight (default: 1.0) }, "tiered": { "latency_exp": 2.0, // Non-linear latency exponent (default: 2.0) "success_rate_exp": 1.0, // Success rate exponent (default: 1.0) "load_exp": 1.0, // Load penalty exponent (default: 1.0) "tier_boundaries": [0.05, 0.2], // Latency thresholds in seconds (default: [0.05, 0.2]) "tier_weights": [70, 20, 10], // Traffic share per tier (default: [70, 20, 10]) "max_conns_per_upstream": 0, // Hard cap per upstream, 0 = unlimited (default: 0) "dynamic": { "enabled": true, "min_latency_exp": 1.0, "max_latency_exp": 4.0, "adaptation_rate": 0.1, "sampling_window": 256, "percentile_method": "p90", "auto_tiers": true, "stability_threshold": 0.05, "rebalance_interval": 10, "explore_ratio": 0.02 } } }, // --- Connection Tuning --- "connection": { "buffer_size": 65536, // Relay buffer size in bytes (default: 32768) "dial_timeout": 10, // Upstream dial timeout in seconds (default: 5) "idle_timeout": 120 // Client idle timeout in seconds (default: 60) }, // --- Retry Policy --- "retry": { "attempts": 4, // Retry count on upstream failure (default: 2) "jitter_ms": 50 // Max jitter between retries in ms (default: 3) }, // --- System Tuning --- "tuning": { "max_connections": 100000, // Global connection limit (default: 50000) "enable_sharding": true, // Enable SO_REUSEPORT listener sharding (default: false) "shard_per_cpu": true // One shard per CPU core (default: false) } }