PROXY protocol
HAProxy's PROXY protocol, versions 1 and 2, read and written: the real client behind a load balancer, with TLVs for TLS, SNI, unique ids, checksums and the AWS, Azure and Google Cloud extras.
Version 1.0.0 · 7 functions · 1 crate
Functions
What use proxy_protocol.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use proxy_parse, proxy_parse_bytes from proxy_protocol (Quillo 0.6).
proxy_parse(base64: str) → anyor an errorThe PROXY header (v1 or v2, found by itself) at the start of base64 (bytes as a load balancer sent them): { version, command: "proxy" | "local", protocol: "TCP4" | "TCP6" | "UDP4" | "UDP6" | "UNIX" | "UNIX_DGRAM" | "UNSPEC" | "UNKNOWN", source, source_port, destination, destination_port, tlvs, header_length, rest_base64, rest_text }. tlvs may have alpn, authority, unique_id (and unique_id_hex), ssl: { client_ssl, client_cert_conn, client_cert_sess, verify, verified, version, cn, cipher, sig_alg, key_alg }, netns, noop (its length), crc32c (checked: a wrong one is an error), aws_vpce_id, azure_link_id, gcp_psc_id, and custom: [{ type, base64 }].
proxy_parse_bytes(bytes: list) → anyor an errorThe same from bytes as a list of numbers, such as a udp.listen packet's p.bytes: the payload after the header is in rest_text (or rest_base64).
proxy_parse_text(text: str) → anyor an errorA version 1 line as text (PROXY TCP4 203.0.113.7 10.0.0.2 51234 443\r\n, CRLF optional here), read as proxy_parse reads it.
proxy_v1(info: any) → stror an errorA version 1 header line: info is { protocol: "TCP4" | "TCP6" | "UNKNOWN" (found from the address when left out), source, source_port, destination, destination_port }.
proxy_v2(info: any) → stror an errorA version 2 header, in base64: info is { command: "proxy" (default) | "local", protocol: "TCP4" | "TCP6" | "UDP4" | "UDP6" | "UNIX" | "UNIX_DGRAM" | "UNSPEC", source, source_port, destination, destination_port, tlvs: { alpn, authority, unique_id, ssl: { client_ssl, client_cert_conn, client_cert_sess, verified, version, cn, cipher, sig_alg, key_alg }, netns, aws_vpce_id, azure_link_id, gcp_psc_id, custom: [{ type, base64 | text }], noop: padding bytes, crc32c: true } }.
proxy_packet(info: any, payload: str) → listor an errorA header (info.version 1 or 2, 2 when left out) followed by payload, as a list of byte values for udp.send: a datagram sent through to a server that expects the PROXY protocol.
proxy_send(addr: str, info: any, data: str, timeout_ms: int) → anyor an errorConnects to addr (host:port, a server behind which the PROXY protocol is on, such as nginx's listen ... proxy_protocol), sends the header for info (version 1 or 2) and then data, and reads the answer until the server closes or timeout_ms (100 to 60000) passes: { header_bytes, sent, reply, reply_base64 }.
Get it
$ quillo install proxy_protocol # proxy_protocol.qwer into this folder$ quillo install proxy_protocol --example # and its example, proxy_protocol.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/proxy_protocol.qwer$ curl -O https://quillolang.org/plugins/files/proxy_protocol.qweThen, in your program, all of it or only what you call:
use proxy_protocol.qweruse proxy_parse, proxy_parse_bytes from proxy_protocol // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
base64 = "0.22"Example
// The PROXY protocol, which load balancers (HAProxy, AWS NLB, Google Cloud, Azure, nginx) put in// front of a connection to say who the client really was: headers read and built, a UDP listener// behind a balancer that sends version 2 on each datagram, and a header sent through to a server.//// quillo run proxy_protocol.qwe then open http://localhost:8080/read, /build and /udp// BACKEND=127.0.0.1:8443 quillo run proxy_protocol.qwe and /send (nginx with `listen 8443 proxy_protocol`)use "proxy_protocol.qwer"// A header as an AWS Network Load Balancer sends it: TCP over IPv4, the VPC endpoint it came// through, and a CRC32C checksum.let NLB = "DQoNCgANClFVSVQKIREALcsAcQcKAAHEw8sBu+oAFwF2cGNlLTA4ZDJiZjE1ZmFjM2Q3OTAzAwAEIaRA9w=="route GET /read { let nlb = proxy_parse(NLB)? let v1 = proxy_parse_text("PROXY TCP6 2001:db8::1 2001:db8::2 51234 443\r\nGET / HTTP/1.1\r\n")? { client: `${nlb.source}:${nlb.source_port}`, endpoint: nlb.tlvs.aws_vpce_id, checksum_ok: nlb.tlvs.crc32c_valid, v1: { protocol: v1.protocol, client: v1.source, port: v1.source_port, then: v1.rest_text }, bad_port: proxy_parse_text("PROXY TCP4 10.0.0.1 10.0.0.2 080 443").error, not_proxy: proxy_parse_text("GET / HTTP/1.1").error, }}// What a TLS-terminating balancer passes on: the client, the SNI it asked for, and its certificate.route GET /build { let info = { protocol: "TCP4", source: "198.51.100.23", source_port: 50112, destination: "10.0.1.5", destination_port: 443, tlvs: { alpn: "h2", authority: "shop.example.com", unique_id: "req-7f3a", ssl: { client_cert_conn: true, verified: true, version: "TLSv1.3", cn: "device-0042", cipher: "TLS_AES_128_GCM_SHA256" }, crc32c: true, }, } let v2 = proxy_v2(info)? let back = proxy_parse(v2)? { v1: proxy_v1(info)?, v2, back: { authority: back.tlvs.authority, cn: back.tlvs.ssl.cn, verified: back.tlvs.ssl.verified, checksum_ok: back.tlvs.crc32c_valid }, tampered: proxy_parse(replace(v2, "c2hvcC", "c2hvcB")).error, }}// UDP behind a balancer: each datagram starts with a version 2 header.let UDP_PORT = int(env("PP_UDP_PORT") ?? "5514")let datagrams = []udp.listen(UDP_PORT, (p) => { let h = proxy_parse_bytes(p.bytes) datagrams = datagrams + [is_err(h) ? { error: h.error } : { client: `${h.source}:${h.source_port}`, protocol: h.protocol, payload: h.rest_text }]}, { host: "127.0.0.1" })route GET /udp { let info = { protocol: "UDP4", source: "203.0.113.9", source_port: 40000, destination: "127.0.0.1", destination_port: UDP_PORT } udp.send("127.0.0.1", UDP_PORT, proxy_packet(info, "temp=21.5")?) udp.send("127.0.0.1", UDP_PORT, "no header here") sleep(200) { datagrams }}// Through to a server that expects the header: nginx answers with the client it was told about.route GET /send { let backend = env("BACKEND", null) ?? reply 503 "set BACKEND to host:port of a server that takes the PROXY protocol" let info = { version: 2, source: "198.51.100.23", source_port: 50112, destination: "10.0.1.5", destination_port: 80, tlvs: { authority: "shop.example.com" } } proxy_send(backend, info, "GET / HTTP/1.0\r\nHost: shop.example.com\r\n\r\n", 3000)?}$ quillo run proxy_protocol.qwe$ curl 'localhost:8080/read'$ curl 'localhost:8080/build'$ curl 'localhost:8080/udp'Source
//! HAProxy's PROXY protocol, versions 1 and 2: headers built and read (TCP4, TCP6, UDP, UNIX, and TLVs: ALPN, authority, unique id, SSL, network namespace, CRC32C and the AWS, Azure and Google Cloud ones), for programs behind a load balancer and for sending through one.//! version: 1.0.0//!//! [dependencies]//! base64 = "0.22"use serde_json::{json, Value};use std::io::{Read, Write};use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream, ToSocketAddrs};use std::time::{Duration, Instant};const SIG: [u8; 12] = [0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A];fn b64() -> base64::engine::GeneralPurpose { base64::engine::general_purpose::STANDARD}fn b64_decode(s: &str) -> Result<Vec<u8>, String> { use base64::Engine; let t: String = s.chars().filter(|c| !c.is_whitespace()).collect(); b64().decode(&t).or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(&t)).map_err(|e| format!("not base64: {}", e))}fn b64_encode(b: &[u8]) -> String { use base64::Engine; b64().encode(b)}/// CRC-32C (Castagnoli), as the v2 checksum TLV uses it.fn crc32c(data: &[u8]) -> u32 { let mut table = [0u32; 256]; for i in 0..256u32 { let mut c = i; for _ in 0..8 { c = if c & 1 != 0 { 0x82F6_3B78 ^ (c >> 1) } else { c >> 1 }; } table[i as usize] = c; } let mut crc = 0xFFFF_FFFFu32; for &b in data { crc = table[((crc ^ b as u32) & 0xFF) as usize] ^ (crc >> 8); } !crc}fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect()}fn text_or_null(b: &[u8]) -> Value { match std::str::from_utf8(b) { Ok(s) => json!(s), Err(_) => Value::Null, }}// ---------------------------------------------------------------- readingfn parse_v1(b: &[u8]) -> Result<(Value, usize), String> { let end = b.windows(2).position(|w| w == b"\r\n").ok_or("a PROXY v1 line ends with CRLF, and none came in the first 107 bytes")?; if end + 2 > 107 { return Err("a PROXY v1 line is at most 107 bytes".into()); } let line = std::str::from_utf8(&b[..end]).map_err(|_| "the PROXY v1 line is not ASCII")?; let parts: Vec<&str> = line.split(' ').collect(); if parts[0] != "PROXY" || parts.len() < 2 { return Err(format!("not a PROXY v1 line: {:?}", line)); } let proto = parts[1]; if proto == "UNKNOWN" { return Ok((json!({ "version": 1, "command": "proxy", "protocol": "UNKNOWN", "source": null, "source_port": null, "destination": null, "destination_port": null, "tlvs": {} }), end + 2)); } if proto != "TCP4" && proto != "TCP6" { return Err(format!("PROXY v1 knows TCP4, TCP6 and UNKNOWN, not {:?}", proto)); } if parts.len() != 6 { return Err(format!("a PROXY v1 {} line has 6 fields (PROXY, protocol, two addresses, two ports), this one {}", proto, parts.len())); } let ip = |s: &str| -> Result<IpAddr, String> { let a: IpAddr = s.parse().map_err(|_| format!("{:?} is not an IP address", s))?; if (proto == "TCP4") != a.is_ipv4() { return Err(format!("{} is not an {} address", s, if proto == "TCP4" { "IPv4" } else { "IPv6" })); } Ok(a) }; let port = |s: &str| -> Result<u16, String> { if s.is_empty() || s.len() > 5 || !s.bytes().all(|c| c.is_ascii_digit()) || (s.len() > 1 && s.starts_with('0')) { return Err(format!("{:?} is not a port (0 to 65535, no leading zeros)", s)); } s.parse::<u16>().map_err(|_| format!("{:?} is not a port (0 to 65535)", s)) }; let (src, dst, sp, dp) = (ip(parts[2])?, ip(parts[3])?, port(parts[4])?, port(parts[5])?); Ok((json!({ "version": 1, "command": "proxy", "protocol": proto, "source": src.to_string(), "source_port": sp, "destination": dst.to_string(), "destination_port": dp, "tlvs": {} }), end + 2))}fn parse_tlvs(mut t: &[u8], out: &mut serde_json::Map<String, Value>, inside_ssl: bool) -> Result<(), String> { let mut other = vec![]; while !t.is_empty() { if t.len() < 3 { return Err("a TLV is cut short (fewer than 3 bytes left)".into()); } let ty = t[0]; let len = u16::from_be_bytes([t[1], t[2]]) as usize; if t.len() < 3 + len { return Err(format!("TLV 0x{:02X} says {} bytes, {} are left", ty, len, t.len() - 3)); } let v = &t[3..3 + len]; t = &t[3 + len..]; let s = |v: &[u8]| text_or_null(v); match (ty, inside_ssl) { (0x01, false) => { out.insert("alpn".into(), s(v)); } (0x02, false) => { out.insert("authority".into(), s(v)); } (0x03, false) => { /* checked by the caller */ out.insert("crc32c".into(), json!(hex(v))); } (0x04, false) => { out.insert("noop".into(), json!(len)); } (0x05, false) => { if len > 128 { return Err("a unique id is at most 128 bytes".into()); } out.insert("unique_id".into(), s(v)); out.insert("unique_id_hex".into(), json!(hex(v))); } (0x20, false) => { if len < 5 { return Err("the SSL TLV is at least 5 bytes (client flags and verify)".into()); } let client = v[0]; let verify = u32::from_be_bytes([v[1], v[2], v[3], v[4]]); let mut ssl = serde_json::Map::new(); ssl.insert("client_ssl".into(), json!(client & 1 != 0)); ssl.insert("client_cert_conn".into(), json!(client & 2 != 0)); ssl.insert("client_cert_sess".into(), json!(client & 4 != 0)); ssl.insert("verify".into(), json!(verify)); ssl.insert("verified".into(), json!(verify == 0 && client & 6 != 0)); parse_tlvs(&v[5..], &mut ssl, true)?; out.insert("ssl".into(), Value::Object(ssl)); } (0x21, true) => { out.insert("version".into(), s(v)); } (0x22, true) => { out.insert("cn".into(), s(v)); } (0x23, true) => { out.insert("cipher".into(), s(v)); } (0x24, true) => { out.insert("sig_alg".into(), s(v)); } (0x25, true) => { out.insert("key_alg".into(), s(v)); } (0x30, false) => { out.insert("netns".into(), s(v)); } // Cloud load balancers' own TLVs. (0xEA, false) if !v.is_empty() && v[0] == 0x01 => { out.insert("aws_vpce_id".into(), s(&v[1..])); } (0xEE, false) if len == 5 && v[0] == 0x01 => { out.insert("azure_link_id".into(), json!(u32::from_le_bytes([v[1], v[2], v[3], v[4]]))); } (0xE0, false) if len == 8 => { let n = u64::from_be_bytes([v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]]); out.insert("gcp_psc_id".into(), json!(n.to_string())); } _ => other.push(json!({ "type": ty, "base64": b64_encode(v) })), } } if !other.is_empty() { out.insert("custom".into(), Value::Array(other)); } Ok(())}fn parse_v2(b: &[u8]) -> Result<(Value, usize), String> { if b.len() < 16 { return Err(format!("a PROXY v2 header is at least 16 bytes, {} came", b.len())); } let ver = b[12] >> 4; if ver != 2 { return Err(format!("PROXY v2's version nibble is 2, this one is {}", ver)); } let command = match b[12] & 0x0F { 0 => "local", 1 => "proxy", c => return Err(format!("unknown PROXY v2 command {}", c)) }; let fam = b[13] >> 4; let tr = b[13] & 0x0F; let len = u16::from_be_bytes([b[14], b[15]]) as usize; if b.len() < 16 + len { return Err(format!("the PROXY v2 header says {} more bytes, {} came", len, b.len() - 16)); } let body = &b[16..16 + len]; let transport = match tr { 0 => "unspec", 1 => "stream", 2 => "dgram", _ => return Err(format!("unknown PROXY v2 transport {}", tr)) }; let (family, alen) = match fam { 0 => ("unspec", 0), 1 => ("inet", 12), 2 => ("inet6", 36), 3 => ("unix", 216), _ => return Err(format!("unknown PROXY v2 address family {}", fam)) }; if body.len() < alen { return Err(format!("the {} addresses take {} bytes, the header has {}", family, alen, body.len())); } let a = &body[..alen]; let protocol = match (fam, tr) { (1, 1) => "TCP4", (1, 2) => "UDP4", (2, 1) => "TCP6", (2, 2) => "UDP6", (3, 1) => "UNIX", (3, 2) => "UNIX_DGRAM", _ => "UNSPEC", }; let (mut src, mut dst, mut sp, mut dp) = (Value::Null, Value::Null, Value::Null, Value::Null); match fam { 1 => { src = json!(Ipv4Addr::new(a[0], a[1], a[2], a[3]).to_string()); dst = json!(Ipv4Addr::new(a[4], a[5], a[6], a[7]).to_string()); sp = json!(u16::from_be_bytes([a[8], a[9]])); dp = json!(u16::from_be_bytes([a[10], a[11]])); } 2 => { let s: [u8; 16] = a[0..16].try_into().unwrap(); let d: [u8; 16] = a[16..32].try_into().unwrap(); src = json!(Ipv6Addr::from(s).to_string()); dst = json!(Ipv6Addr::from(d).to_string()); sp = json!(u16::from_be_bytes([a[32], a[33]])); dp = json!(u16::from_be_bytes([a[34], a[35]])); } 3 => { let path = |p: &[u8]| text_or_null(&p[..p.iter().position(|&c| c == 0).unwrap_or(p.len())]); src = path(&a[0..108]); dst = path(&a[108..216]); } _ => {} } let mut tlvs = serde_json::Map::new(); parse_tlvs(&body[alen..], &mut tlvs, false)?; if tlvs.contains_key("crc32c") { // Find the checksum TLV again, zero it, and check the whole header. let mut copy = b[..16 + len].to_vec(); let mut i = 16 + alen; while i + 3 <= copy.len() { let l = u16::from_be_bytes([copy[i + 1], copy[i + 2]]) as usize; if copy[i] == 0x03 && l == 4 { let want = u32::from_be_bytes([copy[i + 3], copy[i + 4], copy[i + 5], copy[i + 6]]); for k in 0..4 { copy[i + 3 + k] = 0; } let got = crc32c(©); if got != want { return Err(format!("the header's CRC32C is {:08x} but its bytes give {:08x}: it was changed on the way; drop the connection", want, got)); } tlvs.insert("crc32c_valid".into(), json!(true)); } i += 3 + l; } } Ok((json!({ "version": 2, "command": command, "protocol": protocol, "family": family, "transport": transport, "source": src, "source_port": sp, "destination": dst, "destination_port": dp, "tlvs": Value::Object(tlvs), }), 16 + len))}fn parse_any(b: &[u8]) -> Result<Value, String> { let (mut v, used) = if b.len() >= 12 && b[..12] == SIG { parse_v2(b)? } else if b.starts_with(b"PROXY ") { parse_v1(b)? } else if b.len() < 12 && (SIG.starts_with(b) || b"PROXY ".starts_with(b)) { return Err(format!("only {} bytes: read more before parsing the PROXY header", b.len())); } else { return Err("no PROXY header: the bytes start with neither \"PROXY \" (v1) nor the v2 signature".into()); }; let rest = &b[used..]; v["header_length"] = json!(used); v["rest_base64"] = json!(b64_encode(rest)); v["rest_text"] = text_or_null(rest); Ok(v)}/// The PROXY header (v1 or v2, found by itself) at the start of `base64` (bytes as a load balancer sent them): `{ version, command: "proxy" | "local", protocol: "TCP4" | "TCP6" | "UDP4" | "UDP6" | "UNIX" | "UNIX_DGRAM" | "UNSPEC" | "UNKNOWN", source, source_port, destination, destination_port, tlvs, header_length, rest_base64, rest_text }`. `tlvs` may have `alpn`, `authority`, `unique_id` (and `unique_id_hex`), `ssl: { client_ssl, client_cert_conn, client_cert_sess, verify, verified, version, cn, cipher, sig_alg, key_alg }`, `netns`, `noop` (its length), `crc32c` (checked: a wrong one is an error), `aws_vpce_id`, `azure_link_id`, `gcp_psc_id`, and `custom: [{ type, base64 }]`.pub fn proxy_parse(base64: &str) -> Result<Value, String> { parse_any(&b64_decode(base64)?)}/// The same from bytes as a list of numbers, such as a `udp.listen` packet's `p.bytes`: the payload after the header is in `rest_text` (or `rest_base64`).pub fn proxy_parse_bytes(bytes: Vec<i64>) -> Result<Value, String> { let b: Vec<u8> = bytes.iter().map(|&x| u8::try_from(x).map_err(|_| format!("{} is not a byte (0 to 255)", x))).collect::<Result<_, _>>()?; parse_any(&b)}/// A version 1 line as text (`PROXY TCP4 203.0.113.7 10.0.0.2 51234 443\r\n`, CRLF optional here), read as `proxy_parse` reads it.pub fn proxy_parse_text(text: &str) -> Result<Value, String> { let mut t = text.to_string(); if !t.contains("\r\n") { t = match t.find('\n') { Some(i) => format!("{}\r\n{}", &t[..i], &t[i + 1..]), None => format!("{}\r\n", t) }; } parse_any(t.as_bytes())}// ---------------------------------------------------------------- buildingfn field<'a>(info: &'a Value, names: &[&str]) -> &'a Value { for n in names { if !info[*n].is_null() { return &info[*n]; } } &Value::Null}fn port_of(info: &Value, names: &[&str]) -> Result<u16, String> { let v = field(info, names); v.as_u64().and_then(|n| u16::try_from(n).ok()).ok_or(format!("`{}` must be a port from 0 to 65535, not {}", names[0], v))}fn protocol_of(info: &Value) -> Result<String, String> { if let Some(p) = info["protocol"].as_str() { return Ok(p.to_uppercase()); } match field(info, &["source", "src"]).as_str().map(|s| s.parse::<IpAddr>()) { Some(Ok(IpAddr::V4(_))) => Ok("TCP4".into()), Some(Ok(IpAddr::V6(_))) => Ok("TCP6".into()), Some(Err(_)) => Ok("UNIX".into()), None => Ok("UNKNOWN".into()), }}fn v1(info: &Value) -> Result<String, String> { let proto = protocol_of(info)?; if proto == "UNKNOWN" || proto == "UNSPEC" { return Ok("PROXY UNKNOWN\r\n".into()); } if proto != "TCP4" && proto != "TCP6" { return Err(format!("PROXY v1 carries TCP4, TCP6 or UNKNOWN; {} needs version 2", proto)); } let ip = |names: &[&str]| -> Result<IpAddr, String> { let s = field(info, names).as_str().ok_or(format!("`{}` is missing: the address, as text", names[0]))?; let a: IpAddr = s.parse().map_err(|_| format!("`{}`: {:?} is not an IP address", names[0], s))?; if a.is_ipv4() != (proto == "TCP4") { return Err(format!("`{}`: {} is not an {} address", names[0], s, if proto == "TCP4" { "IPv4" } else { "IPv6" })); } Ok(a) }; Ok(format!("PROXY {} {} {} {} {}\r\n", proto, ip(&["source", "src"])?, ip(&["destination", "dest", "dst"])?, port_of(info, &["source_port", "src_port"])?, port_of(info, &["destination_port", "dest_port", "dst_port"])?))}fn tlv(out: &mut Vec<u8>, ty: u8, v: &[u8]) -> Result<(), String> { if v.len() > 65535 - 3 { return Err(format!("TLV 0x{:02X} is too long", ty)); } out.push(ty); out.extend_from_slice(&(v.len() as u16).to_be_bytes()); out.extend_from_slice(v); Ok(())}fn text_tlv(out: &mut Vec<u8>, t: &Value, name: &str, ty: u8) -> Result<(), String> { match &t[name] { Value::Null => Ok(()), Value::String(s) => tlv(out, ty, s.as_bytes()), other => Err(format!("TLV `{}` is text, not {}", name, other)), }}fn v2(info: &Value) -> Result<Vec<u8>, String> { let command = match info["command"].as_str().unwrap_or("proxy").to_lowercase().as_str() { "proxy" => 1u8, "local" => 0u8, c => return Err(format!("`command` is \"proxy\" or \"local\", not {:?}", c)) }; let proto = protocol_of(info)?; let (fam, tr) = match proto.as_str() { "TCP4" => (1u8, 1u8), "UDP4" => (1, 2), "TCP6" => (2, 1), "UDP6" => (2, 2), "UNIX" => (3, 1), "UNIX_DGRAM" => (3, 2), "UNSPEC" | "UNKNOWN" => (0, 0), p => return Err(format!("`protocol` is one of TCP4, TCP6, UDP4, UDP6, UNIX, UNIX_DGRAM or UNSPEC, not {:?}", p)), }; let mut body = vec![]; let addr = |names: &[&str]| field(info, names).as_str().map(|s| s.to_string()).ok_or(format!("`{}` is missing", names[0])); match fam { 1 | 2 => { let s: IpAddr = addr(&["source", "src"])?.parse().map_err(|_| "`source` is not an IP address".to_string())?; let d: IpAddr = addr(&["destination", "dest", "dst"])?.parse().map_err(|_| "`destination` is not an IP address".to_string())?; match (s, d, fam) { (IpAddr::V4(s), IpAddr::V4(d), 1) => { body.extend_from_slice(&s.octets()); body.extend_from_slice(&d.octets()); } (IpAddr::V6(s), IpAddr::V6(d), 2) => { body.extend_from_slice(&s.octets()); body.extend_from_slice(&d.octets()); } _ => return Err(format!("{} needs two {} addresses", proto, if fam == 1 { "IPv4" } else { "IPv6" })), } body.extend_from_slice(&port_of(info, &["source_port", "src_port"])?.to_be_bytes()); body.extend_from_slice(&port_of(info, &["destination_port", "dest_port", "dst_port"])?.to_be_bytes()); } 3 => { for names in [&["source", "src"][..], &["destination", "dest", "dst"][..]] { let p = addr(names)?; if p.len() > 107 { return Err(format!("`{}`: a UNIX socket path is at most 107 bytes", names[0])); } let mut b = p.into_bytes(); b.resize(108, 0); body.extend_from_slice(&b); } } _ => {} } let t = &info["tlvs"]; if !t.is_null() && !t.is_object() { return Err("`tlvs` is a map".into()); } text_tlv(&mut body, t, "alpn", 0x01)?; text_tlv(&mut body, t, "authority", 0x02)?; if let Some(u) = t["unique_id"].as_str() { if u.len() > 128 { return Err("`unique_id` is at most 128 bytes".into()); } tlv(&mut body, 0x05, u.as_bytes())?; } if t["ssl"].is_object() { let s = &t["ssl"]; let flag = |k: &str| s[k].as_bool().unwrap_or(false); let client = (if flag("client_ssl") || s["client_ssl"].is_null() { 1u8 } else { 0 }) | (if flag("client_cert_conn") { 2 } else { 0 }) | (if flag("client_cert_sess") { 4 } else { 0 }); let verify: u32 = match (&s["verify"], &s["verified"]) { (Value::Number(n), _) => n.as_u64().and_then(|x| u32::try_from(x).ok()).ok_or("`ssl.verify` is a number from 0 up")?, (_, Value::Bool(b)) => if *b { 0 } else { 1 }, _ => if client & 6 != 0 { 0 } else { 1 }, }; let mut v = vec![client]; v.extend_from_slice(&verify.to_be_bytes()); for (k, ty) in [("version", 0x21u8), ("cn", 0x22), ("cipher", 0x23), ("sig_alg", 0x24), ("key_alg", 0x25)] { text_tlv(&mut v, s, k, ty)?; } tlv(&mut body, 0x20, &v)?; } text_tlv(&mut body, t, "netns", 0x30)?; if let Some(id) = t["aws_vpce_id"].as_str() { let mut v = vec![0x01]; v.extend_from_slice(id.as_bytes()); tlv(&mut body, 0xEA, &v)?; } if let Some(id) = t["azure_link_id"].as_u64() { let mut v = vec![0x01]; v.extend_from_slice(&u32::try_from(id).map_err(|_| "`azure_link_id` is a 32-bit number")?.to_le_bytes()); tlv(&mut body, 0xEE, &v)?; } match &t["gcp_psc_id"] { Value::Null => {} v => { let n: u64 = match v { Value::String(s) => s.parse().map_err(|_| "`gcp_psc_id` is a 64-bit number")?, Value::Number(n) => n.as_u64().ok_or("`gcp_psc_id` is a 64-bit number")?, _ => return Err("`gcp_psc_id` is a 64-bit number".into()) }; tlv(&mut body, 0xE0, &n.to_be_bytes())?; } } if let Some(list) = t["custom"].as_array() { for c in list { let ty = c["type"].as_u64().filter(|&x| x <= 255).ok_or("each `custom` TLV has a `type` from 0 to 255")? as u8; let v = match (&c["base64"], &c["text"]) { (Value::String(b), _) => b64_decode(b)?, (_, Value::String(s)) => s.as_bytes().to_vec(), _ => vec![] }; tlv(&mut body, ty, &v)?; } } if let Some(n) = t["noop"].as_u64() { tlv(&mut body, 0x04, &vec![0u8; n.min(1024) as usize])?; } let want_crc = t["crc32c"].as_bool().unwrap_or(false); let crc_at = if want_crc { body.len() + 16 + 3 } else { 0 }; if want_crc { tlv(&mut body, 0x03, &[0, 0, 0, 0])?; } if body.len() > 65535 { return Err("the header is longer than 65535 bytes".into()); } let mut out = SIG.to_vec(); out.push(0x20 | command); out.push((fam << 4) | tr); out.extend_from_slice(&(body.len() as u16).to_be_bytes()); out.extend_from_slice(&body); if want_crc { let c = crc32c(&out); out[crc_at..crc_at + 4].copy_from_slice(&c.to_be_bytes()); } Ok(out)}fn header(info: &Value) -> Result<Vec<u8>, String> { match info["version"].as_i64().unwrap_or(2) { 1 => Ok(v1(info)?.into_bytes()), 2 => v2(info), v => Err(format!("`version` is 1 or 2, not {}", v)), }}/// A version 1 header line: `info` is `{ protocol: "TCP4" | "TCP6" | "UNKNOWN" (found from the address when left out), source, source_port, destination, destination_port }`.pub fn proxy_v1(info: serde_json::Value) -> Result<String, String> { v1(&info)}/// A version 2 header, in base64: `info` is `{ command: "proxy" (default) | "local", protocol: "TCP4" | "TCP6" | "UDP4" | "UDP6" | "UNIX" | "UNIX_DGRAM" | "UNSPEC", source, source_port, destination, destination_port, tlvs: { alpn, authority, unique_id, ssl: { client_ssl, client_cert_conn, client_cert_sess, verified, version, cn, cipher, sig_alg, key_alg }, netns, aws_vpce_id, azure_link_id, gcp_psc_id, custom: [{ type, base64 | text }], noop: padding bytes, crc32c: true } }`.pub fn proxy_v2(info: serde_json::Value) -> Result<String, String> { Ok(b64_encode(&v2(&info)?))}/// A header (`info.version` 1 or 2, 2 when left out) followed by `payload`, as a list of byte values for `udp.send`: a datagram sent through to a server that expects the PROXY protocol.pub fn proxy_packet(info: serde_json::Value, payload: &str) -> Result<Vec<i64>, String> { let mut b = header(&info)?; b.extend_from_slice(payload.as_bytes()); Ok(b.into_iter().map(|x| x as i64).collect())}fn resolve(addr: &str) -> Result<SocketAddr, String> { addr.to_socket_addrs().map_err(|e| format!("{}: {} (write host:port)", addr, e))?.next().ok_or(format!("{} has no address", addr))}/// Connects to `addr` (`host:port`, a server behind which the PROXY protocol is on, such as nginx's `listen ... proxy_protocol`), sends the header for `info` (version 1 or 2) and then `data`, and reads the answer until the server closes or `timeout_ms` (100 to 60000) passes: `{ header_bytes, sent, reply, reply_base64 }`.pub fn proxy_send(addr: &str, info: serde_json::Value, data: &str, timeout_ms: i64) -> Result<Value, String> { let t = Duration::from_millis(timeout_ms.clamp(100, 60_000) as u64); let a = resolve(addr)?; let h = header(&info)?; let mut s = TcpStream::connect_timeout(&a, t).map_err(|e| format!("cannot connect to {}: {}", a, e))?; s.set_write_timeout(Some(t)).map_err(|e| e.to_string())?; let mut all = h.clone(); all.extend_from_slice(data.as_bytes()); s.write_all(&all).map_err(|e| format!("sending to {}: {}", a, e))?; let until = Instant::now() + t; let mut out = vec![]; let mut buf = [0u8; 8192]; loop { let left = until.saturating_duration_since(Instant::now()); if left.is_zero() || out.len() > 16 << 20 { break; } let _ = s.set_read_timeout(Some(left.max(Duration::from_millis(1)))); match s.read(&mut buf) { Ok(0) => break, Ok(n) => out.extend_from_slice(&buf[..n]), Err(e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => break, Err(e) => { if out.is_empty() { return Err(format!("{} closed the connection: {} (does it expect this PROXY version?)", a, e)); } break; } } } Ok(json!({ "header_bytes": h.len(), "sent": all.len(), "reply": String::from_utf8_lossy(&out), "reply_base64": b64_encode(&out) }))}Related
iso8583.qwer · 1.0.0ISO 8583 card payment messages packed and unpacked from a field spec given as data (fixed and variable fields in ASCII, BCD or binary, both bitmaps), with the 1987 fields built in, answers explained and sent to a host over TCP.7 views · 7 downloadsRADIUS radius.qwer · 1.0.0RADIUS sign-in for Wi-Fi, VPNs and terminals: PAP and CHAP with a Message-Authenticator, accept, reject and challenge answers read with their attributes (vendor ones too), accounting start to stop, Status-Server; and a server of your own on udp.listen.7 views · 7 downloadsContent-Security-Policy csp.qwer · 1.0.0Content-Security-Policy headers built from a map and read back, a nonce per page, the policy graded for the holes that let scripts in, and whether a URL would load under it.7 views · 7 downloadsSecurity headers security_headers.qwer · 1.0.0A response's security headers graded A+ to F: HSTS, CSP, framing, sniffing, referrers, permissions, cookies' flags, CORS and version leaks, each with its fix; and a good set to send.8 views · 6 downloadsModbus frames modbus.qwer · 1.0.0Modbus RTU and TCP frames built and read without a device: register and coil requests, answers and exceptions explained, CRC-16 and MBAP headers, registers turned into integers, floats and text in any word order and back, and a TCP client for a real PLC or meter.8 views · 10 downloadsBonjour and mDNS mdns.qwer · 1.0.0Bonjour on the LAN: printers, web panels and screens found by service type with their host, port, addresses and TXT records, the network's service types listed, .local names resolved, your own services advertised, and a watch that sees them come and go.7 views · 9 downloadsThread 0 posts
On PROXY protocol: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.