npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@push.rocks/smartvpn

v1.19.2

Published

A VPN solution with TypeScript control plane and Rust data plane daemon

Downloads

877

Readme

PROXY Protocol v2 Support for SmartVPN WebSocket Transport

Context

SmartVPN's WebSocket transport is designed to sit behind reverse proxies (Cloudflare, HAProxy, SmartProxy). The recently added ACL engine has ipAllowList/ipBlockList per client, but without PROXY protocol support the server only sees the proxy's IP — not the real client's. This makes source-IP ACLs useless behind a proxy.

PROXY protocol v2 solves this by letting the proxy prepend a binary header with the real client IP/port before the WebSocket upgrade.


Design

Two-Phase ACL with Real Client IP

TCP accept → Read PP v2 header → Extract real IP
  │
  ├─ Phase 1 (pre-handshake): Check server-level connectionIpBlockList → reject early
  │
  ├─ WebSocket upgrade → Noise IK handshake → Client identity known
  │
  └─ Phase 2 (post-handshake): Check per-client ipAllowList/ipBlockList → reject if denied
  • Phase 1: Server-wide block list (connectionIpBlockList on IVpnServerConfig). Rejects before any crypto work. Protects server resources.
  • Phase 2: Per-client ACL from IClientSecurity.ipAllowList/ipBlockList. Applied after the Noise IK handshake identifies the client.

No New Dependencies

PROXY protocol v2 is a fixed-format binary header (16-byte signature + variable address block). Manual parsing (~80 lines) follows the same pattern as codec.rs. No crate needed.

Scope: WebSocket Only

  • WebSocket: Needs PP v2 (sits behind reverse proxies)
  • QUIC: Direct UDP, just use conn.remote_address()
  • WireGuard: Direct UDP, uses boringtun peer tracking

Implementation

Phase 1: New Rust module proxy_protocol.rs

New file: rust/src/proxy_protocol.rs

PP v2 binary format:

Bytes 0-11:   Signature  \x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A
Byte 12:      Version (high nibble = 0x2) | Command (low nibble: 0x0=LOCAL, 0x1=PROXY)
Byte 13:      Address family | Protocol (0x11 = IPv4/TCP, 0x21 = IPv6/TCP)
Bytes 14-15:  Address data length (big-endian u16)
Bytes 16+:    IPv4: 4 src_ip + 4 dst_ip + 2 src_port + 2 dst_port (12 bytes)
              IPv6: 16 src_ip + 16 dst_ip + 2 src_port + 2 dst_port (36 bytes)
pub struct ProxyHeader {
    pub src_addr: SocketAddr,
    pub dst_addr: SocketAddr,
    pub is_local: bool,  // LOCAL command = health check probe
}

/// Read and parse a PROXY protocol v2 header from a TCP stream.
/// Reads exactly the header bytes — the stream is clean for WS upgrade after.
pub async fn read_proxy_header(stream: &mut TcpStream) -> Result<ProxyHeader>
  • 5-second timeout on header read (constant PROXY_HEADER_TIMEOUT)
  • Validates 12-byte signature, version nibble, command type
  • Parses IPv4 and IPv6 address blocks
  • LOCAL command returns is_local: true (caller closes connection gracefully)
  • Unit tests: valid IPv4/IPv6 headers, LOCAL command, invalid signature, truncated data

Modify: rust/src/lib.rs — add pub mod proxy_protocol;

Phase 2: Server config + client info fields

File: rust/src/server.rsServerConfig

Add:

/// Enable PROXY protocol v2 parsing on WebSocket connections.
/// SECURITY: Must be false when accepting direct client connections.
pub proxy_protocol: Option<bool>,
/// Server-level IP block list — applied at TCP accept time, before Noise handshake.
pub connection_ip_block_list: Option<Vec<String>>,

File: rust/src/server.rsClientInfo

Add:

/// Real client IP:port (from PROXY protocol header or direct TCP connection).
pub remote_addr: Option<String>,

Phase 3: ACL helper

File: rust/src/acl.rs

Add a public function for the server-level pre-handshake check:

/// Check whether a connection source IP is in a block list.
pub fn is_connection_blocked(ip: Ipv4Addr, block_list: &[String]) -> bool {
    ip_matches_any(ip, block_list)
}

(Keeps ip_matches_any private; exposes only the specific check needed.)

Phase 4: WebSocket listener integration

File: rust/src/server.rsrun_ws_listener()

Between listener.accept() and transport::accept_connection():

// Determine real client address
let remote_addr = if state.config.proxy_protocol.unwrap_or(false) {
    match proxy_protocol::read_proxy_header(&mut tcp_stream).await {
        Ok(header) if header.is_local => {
            // Health check probe — close gracefully
            return;
        }
        Ok(header) => {
            info!("PP v2: real client {} -> {}", header.src_addr, header.dst_addr);
            Some(header.src_addr)
        }
        Err(e) => {
            warn!("PP v2 parse failed from {}: {}", tcp_addr, e);
            return; // Drop connection
        }
    }
} else {
    Some(tcp_addr) // Direct connection — use TCP SocketAddr
};

// Pre-handshake server-level block list check
if let (Some(ref block_list), Some(ref addr)) = (&state.config.connection_ip_block_list, &remote_addr) {
    if let std::net::IpAddr::V4(v4) = addr.ip() {
        if acl::is_connection_blocked(v4, block_list) {
            warn!("Connection blocked by server IP block list: {}", addr);
            return;
        }
    }
}

// Then proceed with WS upgrade + handle_client_connection as before

Key correctness note: read_proxy_header reads exactly the PP header bytes via read_exact. The TcpStream is then in a clean state for the WS HTTP upgrade. No buffered wrapper needed.

Phase 5: Update handle_client_connection signature

File: rust/src/server.rs

Change signature:

async fn handle_client_connection(
    state: Arc<ServerState>,
    mut sink: Box<dyn TransportSink>,
    mut stream: Box<dyn TransportStream>,
    remote_addr: Option<std::net::SocketAddr>,  // NEW
) -> Result<()>

After Noise IK handshake + registry lookup (where client_security is available), add connection-level per-client ACL:

if let (Some(ref sec), Some(addr)) = (&client_security, &remote_addr) {
    if let std::net::IpAddr::V4(v4) = addr.ip() {
        if acl::is_connection_blocked(v4, sec.ip_block_list.as_deref().unwrap_or(&[])) {
            anyhow::bail!("Client {} connection denied: source IP {} blocked", registered_client_id, addr);
        }
        if let Some(ref allow) = sec.ip_allow_list {
            if !allow.is_empty() && !acl::is_ip_allowed(v4, allow) {
                anyhow::bail!("Client {} connection denied: source IP {} not in allow list", registered_client_id, addr);
            }
        }
    }
}

Populate remote_addr when building ClientInfo:

remote_addr: remote_addr.map(|a| a.to_string()),

Phase 6: QUIC listener — pass remote addr through

File: rust/src/server.rsrun_quic_listener()

QUIC doesn't use PROXY protocol. Just pass conn.remote_address() through:

let remote = conn.remote_address();
// ...
handle_client_connection(state, Box::new(sink), Box::new(stream), Some(remote)).await

Phase 7: TypeScript interface updates

File: ts/smartvpn.interfaces.ts

Add to IVpnServerConfig:

/** Enable PROXY protocol v2 on incoming WebSocket connections.
 *  Required when behind a reverse proxy that sends PP v2 headers. */
proxyProtocol?: boolean;
/** Server-level IP block list — applied at TCP accept time, before Noise handshake. */
connectionIpBlockList?: string[];

Add to IVpnClientInfo:

/** Real client IP:port (from PROXY protocol or direct TCP). */
remoteAddr?: string;

Phase 8: Tests

Rust unit tests in proxy_protocol.rs:

  • parse_valid_ipv4_header — construct a valid PP v2 header with known IPs, verify parsed correctly
  • parse_valid_ipv6_header — same for IPv6
  • parse_local_command — health check probe returns is_local: true
  • reject_invalid_signature — random bytes rejected
  • reject_truncated_header — short reads fail gracefully
  • reject_v1_header — PROXY v1 text format rejected (we only support v2)

Rust unit tests in acl.rs:

  • is_connection_blocked with various IP patterns

TypeScript tests:

  • Config validation accepts proxyProtocol: true + connectionIpBlockList

Key Files to Modify

| File | Changes | |------|---------| | rust/src/proxy_protocol.rs | NEW — PP v2 parser + tests | | rust/src/lib.rs | Add pub mod proxy_protocol; | | rust/src/server.rs | ServerConfig + ClientInfo fields, run_ws_listener PP integration, handle_client_connection signature + connection ACL, run_quic_listener pass-through | | rust/src/acl.rs | Add is_connection_blocked public function | | ts/smartvpn.interfaces.ts | proxyProtocol, connectionIpBlockList, remoteAddr |


Verification

  1. cargo test — all existing 121 tests + new PP parser tests pass
  2. pnpm test — all 79 TS tests pass (no PP in test setup, just config validation)
  3. Manual: socat or test harness to send a PP v2 header before WS upgrade, verify server logs real IP