-
Notifications
You must be signed in to change notification settings - Fork 49
Description
Problem
All traffic received by CVMs is routed through dstack-gateway, but the gateway cannot pass through the original source IP to the CVM. This causes several critical issues:
- BitTorrent trackers cannot determine the source IP, breaking peer table maintenance
- Policy-based load balancing cannot make routing decisions based on client IP
- Firewall rules and access control lists cannot filter by source IP
- Application logs show gateway IP instead of actual client IP
- Rate limiting and abuse prevention become ineffective
Current Architecture
Traffic Flow
Client → dstack-gateway → WireGuard VPN → CVM
Gateway Routing
- Uses SNI-based routing:
<app_id>[-<port>][s|g].<base_domain>
- Two modes:
- TLS termination (default): Gateway terminates TLS, sees plaintext
- TLS passthrough (
s
suffix): Gateway forwards encrypted traffic
- Traffic forwarding via
bridge()
function ingateway/src/proxy/io_bridge.rs
- Bridge does bidirectional TCP copy without preserving source IP metadata
Why Source IP is Lost
- Gateway acts as a reverse proxy / NAT
- Backend connections originate from gateway's WireGuard IP
- No mechanism to communicate original client IP to the CVM
Proposed Solution: PROXY Protocol v2
Implement support for the PROXY protocol to preserve client source IP.
What is PROXY Protocol?
A protocol developed by HAProxy to safely transport connection information (including client source IP) across multiple layers of NAT or TCP proxies. It adds a small header to the TCP connection containing the original client's address.
Versions:
- v1: Human-readable ASCII format (e.g.,
PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n
) - v2: Binary format with better performance and extensibility (recommended)
Industry adoption: HAProxy, nginx, Stunnel, Varnish, Squid, AWS NLB, etc.
Design Overview
1. Configuration (app-compose.json)
Add a new optional field to specify which ports need PROXY protocol:
{
"manifest_version": 2,
"name": "my-app",
"gateway_enabled": true,
"proxy_protocol_ports": [8080, 3000],
"docker_compose_file": "..."
}
Behavior:
- Empty/omitted list = default behavior (no PROXY headers) ✅ Backward compatible
- Ports in list = gateway injects PROXY v2 header before forwarding
2. Registration Flow
CVM Side (dstack-util/src/system_setup.rs
):
- Read
proxy_protocol_ports
fromapp-compose.json
- Pass it in
RegisterCvmRequest
during CVM registration
Gateway Side (gateway/src/main_service.rs
):
- Store
proxy_protocol_ports
inInstanceInfo
struct - Use this info when routing traffic
3. Traffic Forwarding
Injection Point (gateway/src/proxy/io_bridge.rs
):
When forwarding traffic to a CVM:
- Check if destination port is in
proxy_protocol_ports
- If yes:
- Extract real client IP from TCP connection
- Generate PROXY protocol v2 header
- Prepend header before forwarding data
- If no:
- Forward normally (current behavior)
Both modes supported:
- TLS termination: Inject after TLS handshake, before HTTP data
- TLS passthrough: Inject before TLS ClientHello
Implementation Details
1. Extend Protobuf Definition
File: gateway/rpc/proto/gateway_rpc.proto
message RegisterCvmRequest {
string client_public_key = 1;
repeated uint32 proxy_protocol_ports = 2; // NEW
}
2. Update AppCompose Struct
File: dstack-types/src/lib.rs
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AppCompose {
// ... existing fields ...
#[serde(default)]
pub proxy_protocol_ports: Vec<u16>, // NEW
}
3. Update InstanceInfo
File: gateway/src/models.rs
or gateway/src/main_service.rs
pub struct InstanceInfo {
pub id: String,
pub app_id: String,
pub ip: Ipv4Addr,
pub public_key: String,
pub reg_time: SystemTime,
pub last_seen: SystemTime,
pub connections: Arc<AtomicU64>,
pub proxy_protocol_ports: Vec<u16>, // NEW
}
4. Pass Config During Registration
File: dstack-util/src/system_setup.rs:742
client.register_cvm(RegisterCvmRequest {
client_public_key: wg_pk,
proxy_protocol_ports: self.shared.app_compose.proxy_protocol_ports.clone(), // NEW
})
5. Store Config in Gateway
File: gateway/src/main_service.rs:713
(RegisterCvm handler)
let host_info = InstanceInfo {
// ... existing fields ...
proxy_protocol_ports: request.proxy_protocol_ports.clone(), // NEW
};
6. Inject PROXY Header
File: gateway/src/proxy/io_bridge.rs
Add new function to generate PROXY protocol v2 header:
fn generate_proxy_v2_header(
src_ip: IpAddr,
src_port: u16,
dst_ip: IpAddr,
dst_port: u16,
) -> Vec<u8> {
// PROXY protocol v2 binary format
// Signature: \x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A
// Version + Command: \x21 (v2, PROXY command)
// Family + Protocol: \x11 (IPv4, TCP) or \x21 (IPv6, TCP)
// Length + Addresses
// ... implementation details ...
}
Modify bridge functions:
gateway/src/proxy/tls_terminate.rs:299
(TLS termination mode)gateway/src/proxy/tls_passthough.rs:120
(TLS passthrough mode)
Check port and inject header before forwarding:
pub(crate) async fn proxy_to_app(
state: Proxy,
inbound: TcpStream,
buffer: Vec<u8>,
app_id: &str,
port: u16,
) -> Result<()> {
let addresses = state.lock().select_top_n_hosts(app_id)?;
let (mut outbound, _counter) = connect_multiple_hosts(addresses, port).await?;
// NEW: Check if PROXY protocol is enabled for this port
let instance = state.lock().state.instances.get(app_id);
if let Some(info) = instance {
if info.proxy_protocol_ports.contains(&port) {
let src_addr = inbound.peer_addr()?;
let dst_addr = outbound.peer_addr()?;
let proxy_header = generate_proxy_v2_header(
src_addr.ip(), src_addr.port(),
dst_addr.ip(), dst_addr.port(),
);
outbound.write_all(&proxy_header).await?;
}
}
outbound.write_all(&buffer).await?;
bridge(inbound, outbound, &state.config.proxy).await?;
Ok(())
}
7. CVM Application Configuration
Applications must be configured to accept PROXY protocol:
nginx example:
server {
listen 8080 proxy_protocol;
real_ip_header proxy_protocol;
location / {
# $remote_addr now contains real client IP
}
}
HAProxy example:
frontend web
bind :8080 accept-proxy
# Client IP preserved
Backward Compatibility
✅ Fully backward compatible:
- Default:
proxy_protocol_ports
is empty → no PROXY headers (current behavior) - Only apps that explicitly configure it will get PROXY headers
- Existing apps continue working unchanged
Testing Plan
- Unit tests: PROXY v2 header generation
- Integration tests:
- TLS termination mode with PROXY header
- TLS passthrough mode with PROXY header
- Mixed ports (some with PROXY, some without)
- Real-world test:
- Deploy nginx with
proxy_protocol
enabled - Verify logs show real client IP
- Test BitTorrent tracker functionality
- Deploy nginx with
References
- PROXY Protocol Specification
- HAProxy Documentation
- AWS NLB PROXY Protocol v2
- nginx PROXY Protocol Support
Related Files
gateway/src/proxy/io_bridge.rs
- Main bridge logicgateway/src/proxy/tls_terminate.rs
- TLS termination modegateway/src/proxy/tls_passthough.rs
- TLS passthrough modegateway/src/main_service.rs:713
- RegisterCvm handlergateway/rpc/proto/gateway_rpc.proto
- RPC definitionsdstack-types/src/lib.rs
- AppCompose structdstack-util/src/system_setup.rs:742
- CVM registration