mirror of
https://github.com/fafhrd91/actix-net
synced 2024-11-23 21:51:06 +01:00
PoC proxy protocol stream wrapper
This commit is contained in:
parent
4a7f2c95af
commit
b9877392ab
@ -2,6 +2,7 @@
|
|||||||
members = [
|
members = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-macros",
|
"actix-macros",
|
||||||
|
"actix-proxy-protocol",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-server",
|
"actix-server",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
@ -22,6 +23,7 @@ rust-version = "1.65"
|
|||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
actix-codec = { path = "actix-codec" }
|
actix-codec = { path = "actix-codec" }
|
||||||
actix-macros = { path = "actix-macros" }
|
actix-macros = { path = "actix-macros" }
|
||||||
|
actix-proxy-protocol = { path = "actix-proxy-protocol" }
|
||||||
actix-rt = { path = "actix-rt" }
|
actix-rt = { path = "actix-rt" }
|
||||||
actix-server = { path = "actix-server" }
|
actix-server = { path = "actix-server" }
|
||||||
actix-service = { path = "actix-service" }
|
actix-service = { path = "actix-service" }
|
||||||
|
7
actix-proxy-protocol/CHANGES.md
Normal file
7
actix-proxy-protocol/CHANGES.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Changes
|
||||||
|
|
||||||
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
## 0.0.1 - 2022-xx-xx
|
||||||
|
|
||||||
|
- delete me
|
38
actix-proxy-protocol/Cargo.toml
Executable file
38
actix-proxy-protocol/Cargo.toml
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "actix-proxy-protocol"
|
||||||
|
version = "0.0.1"
|
||||||
|
authors = [
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
|
description = "PROXY protocol utilities"
|
||||||
|
keywords = ["proxy", "protocol", "network", "haproxy", "tcp"]
|
||||||
|
categories = ["network-programming", "asynchronous"]
|
||||||
|
homepage = "https://actix.rs"
|
||||||
|
repository = "https://github.com/actix/actix-net.git"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-service = "2"
|
||||||
|
actix-utils = "3"
|
||||||
|
|
||||||
|
arrayvec = "0.7"
|
||||||
|
crc32fast = "1"
|
||||||
|
futures-core = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||||
|
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||||
|
itoa = "1"
|
||||||
|
tokio = { version = "1.13.1", features = ["sync", "io-util"] }
|
||||||
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
actix-codec = "0.5"
|
||||||
|
actix-rt = "2.6"
|
||||||
|
actix-server = "2"
|
||||||
|
|
||||||
|
bytes = "1"
|
||||||
|
const-str = "0.5"
|
||||||
|
env_logger = "0.9"
|
||||||
|
futures-util = { version = "0.3.7", default-features = false, features = ["sink", "async-await-macro"] }
|
||||||
|
once_cell = "1"
|
||||||
|
pretty_assertions = "1"
|
||||||
|
tokio = { version = "1.13.1", features = ["io-util", "rt-multi-thread", "macros", "fs"] }
|
1
actix-proxy-protocol/LICENSE-APACHE
Symbolic link
1
actix-proxy-protocol/LICENSE-APACHE
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../LICENSE-APACHE
|
1
actix-proxy-protocol/LICENSE-MIT
Symbolic link
1
actix-proxy-protocol/LICENSE-MIT
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../LICENSE-MIT
|
17
actix-proxy-protocol/README.md
Normal file
17
actix-proxy-protocol/README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# actix-proxy-protocol
|
||||||
|
|
||||||
|
> Implementation of the [PROXY protocol].
|
||||||
|
|
||||||
|
[![crates.io](https://img.shields.io/crates/v/actix-proxy-protocol?label=latest)](https://crates.io/crates/actix-proxy-protocol)
|
||||||
|
[![Documentation](https://docs.rs/actix-proxy-protocol/badge.svg?version=0.1.0)](https://docs.rs/actix-proxy-protocol/0.1.0)
|
||||||
|
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||
|
![License](https://img.shields.io/crates/l/actix-proxy-protocol.svg)
|
||||||
|
[![Dependency Status](https://deps.rs/crate/actix-proxy-protocol/0.1.0/status.svg)](https://deps.rs/crate/actix-proxy-protocol/0.1.0)
|
||||||
|
![Downloads](https://img.shields.io/crates/d/actix-proxy-protocol.svg)
|
||||||
|
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Examples](./examples)
|
||||||
|
|
||||||
|
[proxy protocol]: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
118
actix-proxy-protocol/examples/proxy-server.rs
Normal file
118
actix-proxy-protocol/examples/proxy-server.rs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
//! Adds PROXY protocol v1 prelude to connections.
|
||||||
|
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io, mem,
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_proxy_protocol::{v1, v2, AddressFamily, Command, Tlv, TransportProtocol};
|
||||||
|
use actix_rt::net::TcpStream;
|
||||||
|
use actix_server::Server;
|
||||||
|
use actix_service::{fn_service, ServiceFactoryExt as _};
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use const_str::concat_bytes;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use tokio::io::{copy_bidirectional, AsyncReadExt as _, AsyncWriteExt as _};
|
||||||
|
|
||||||
|
static UPSTREAM: Lazy<SocketAddr> = Lazy::new(|| SocketAddr::from(([127, 0, 0, 1], 8080)));
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTES:
|
||||||
|
108 byte buffer on receiver side is enough for any PROXY header
|
||||||
|
after PROXY, receive until CRLF, *then* decode parts
|
||||||
|
TLV = type-length-value
|
||||||
|
|
||||||
|
TO DO:
|
||||||
|
handle UNKNOWN transport
|
||||||
|
v2 UNSPEC mode
|
||||||
|
*/
|
||||||
|
|
||||||
|
fn extend_with_ip_bytes(buf: &mut Vec<u8>, ip: IpAddr) {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ip) => buf.extend_from_slice(&ip.octets()),
|
||||||
|
IpAddr::V6(ip) => buf.extend_from_slice(&ip.octets()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wrap_with_proxy_protocol_v1(mut stream: TcpStream) -> io::Result<()> {
|
||||||
|
let mut upstream = TcpStream::connect(("127.0.0.1", 8080)).await?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"PROXYv1 {} -> {}",
|
||||||
|
stream.peer_addr().unwrap(),
|
||||||
|
UPSTREAM.to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let proxy_header = v1::Header::new(
|
||||||
|
AddressFamily::Inet,
|
||||||
|
SocketAddr::from(([127, 0, 0, 1], 8081)),
|
||||||
|
*UPSTREAM,
|
||||||
|
);
|
||||||
|
|
||||||
|
proxy_header.write_to_tokio(&mut upstream).await?;
|
||||||
|
|
||||||
|
let (_bytes_read, _bytes_written) = copy_bidirectional(&mut stream, &mut upstream).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wrap_with_proxy_protocol_v2(mut stream: TcpStream) -> io::Result<()> {
|
||||||
|
let mut upstream = TcpStream::connect(("127.0.0.1", 8080)).await?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"PROXYv2 {} -> {}",
|
||||||
|
stream.peer_addr().unwrap(),
|
||||||
|
UPSTREAM.to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut proxy_header = v2::Header::new(
|
||||||
|
Command::Proxy,
|
||||||
|
TransportProtocol::Stream,
|
||||||
|
AddressFamily::Inet,
|
||||||
|
SocketAddr::from(([127, 0, 0, 1], 8082)),
|
||||||
|
*UPSTREAM,
|
||||||
|
vec![
|
||||||
|
Tlv::new(0x05, [0x34, 0x32, 0x36, 0x39]), // UNIQUE_ID
|
||||||
|
Tlv::new(0x04, "NOOP m9"), // NOOP
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
proxy_header.add_crc23c_checksum_tlv();
|
||||||
|
|
||||||
|
proxy_header.write_to_tokio(&mut upstream).await?;
|
||||||
|
|
||||||
|
let (_bytes_read, _bytes_written) = copy_bidirectional(&mut stream, &mut upstream).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_server() -> io::Result<Server> {
|
||||||
|
let addr = ("127.0.0.1", 8082);
|
||||||
|
tracing::info!("starting proxy server on port: {}", &addr.0);
|
||||||
|
tracing::info!("proxying to 127.0.0.1:8080");
|
||||||
|
|
||||||
|
Ok(Server::build()
|
||||||
|
.bind("proxy-protocol-v1", ("127.0.0.1", 8081), move || {
|
||||||
|
fn_service(wrap_with_proxy_protocol_v1)
|
||||||
|
.map_err(|err| tracing::error!("service error: {:?}", err))
|
||||||
|
})?
|
||||||
|
.bind("proxy-protocol-v2", addr, move || {
|
||||||
|
fn_service(wrap_with_proxy_protocol_v2)
|
||||||
|
.map_err(|err| tracing::error!("service error: {:?}", err))
|
||||||
|
})?
|
||||||
|
.workers(2)
|
||||||
|
.run())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
|
||||||
|
start_server()?.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
244
actix-proxy-protocol/src/lib.rs
Normal file
244
actix-proxy-protocol/src/lib.rs
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
//! PROXY protocol.
|
||||||
|
|
||||||
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
|
#![warn(future_incompatible)]
|
||||||
|
// #![warn(missing_docs)]
|
||||||
|
#![allow(unused)]
|
||||||
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
convert::TryFrom as _,
|
||||||
|
fmt, io,
|
||||||
|
net::{IpAddr, SocketAddr},
|
||||||
|
};
|
||||||
|
|
||||||
|
use arrayvec::{ArrayString, ArrayVec};
|
||||||
|
use tokio::io::{AsyncWrite, AsyncWriteExt as _};
|
||||||
|
|
||||||
|
pub mod v1;
|
||||||
|
pub mod v2;
|
||||||
|
|
||||||
|
/// PROXY Protocol Version.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum Version {
|
||||||
|
/// Human-readable header format (Version 1)
|
||||||
|
V1,
|
||||||
|
|
||||||
|
/// Binary header format (Version 2)
|
||||||
|
V2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
const fn signature(&self) -> &'static [u8] {
|
||||||
|
match self {
|
||||||
|
Version::V1 => v1::SIGNATURE.as_bytes(),
|
||||||
|
Version::V2 => v2::SIGNATURE.as_slice(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn v2_hi(&self) -> u8 {
|
||||||
|
(match self {
|
||||||
|
Version::V1 => panic!("v1 not supported in PROXY v2"),
|
||||||
|
Version::V2 => 0x2,
|
||||||
|
}) << 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Command
|
||||||
|
///
|
||||||
|
/// other values are unassigned and must not be emitted by senders. Receivers
|
||||||
|
/// must drop connections presenting unexpected values here.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Command {
|
||||||
|
/// \x0 : LOCAL : the connection was established on purpose by the proxy
|
||||||
|
/// without being relayed. The connection endpoints are the sender and the
|
||||||
|
/// receiver. Such connections exist when the proxy sends health-checks to the
|
||||||
|
/// server. The receiver must accept this connection as valid and must use the
|
||||||
|
/// real connection endpoints and discard the protocol block including the
|
||||||
|
/// family which is ignored.
|
||||||
|
Local,
|
||||||
|
|
||||||
|
/// \x1 : PROXY : the connection was established on behalf of another node,
|
||||||
|
/// and reflects the original connection endpoints. The receiver must then use
|
||||||
|
/// the information provided in the protocol block to get original the address.
|
||||||
|
Proxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
const fn v2_lo(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Command::Local => 0x0,
|
||||||
|
Command::Proxy => 0x1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Address Family.
|
||||||
|
///
|
||||||
|
/// maps to the original socket family without necessarily
|
||||||
|
/// matching the values internally used by the system.
|
||||||
|
///
|
||||||
|
/// other values are unspecified and must not be emitted in version 2 of this
|
||||||
|
/// protocol and must be rejected as invalid by receivers.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum AddressFamily {
|
||||||
|
/// 0x0 : AF_UNSPEC : the connection is forwarded for an unknown, unspecified
|
||||||
|
/// or unsupported protocol. The sender should use this family when sending
|
||||||
|
/// LOCAL commands or when dealing with unsupported protocol families. The
|
||||||
|
/// receiver is free to accept the connection anyway and use the real endpoint
|
||||||
|
/// addresses or to reject it. The receiver should ignore address information.
|
||||||
|
Unspecified,
|
||||||
|
|
||||||
|
/// 0x1 : AF_INET : the forwarded connection uses the AF_INET address family
|
||||||
|
/// (IPv4). The addresses are exactly 4 bytes each in network byte order,
|
||||||
|
/// followed by transport protocol information (typically ports).
|
||||||
|
Inet,
|
||||||
|
|
||||||
|
/// 0x2 : AF_INET6 : the forwarded connection uses the AF_INET6 address family
|
||||||
|
/// (IPv6). The addresses are exactly 16 bytes each in network byte order,
|
||||||
|
/// followed by transport protocol information (typically ports).
|
||||||
|
Inet6,
|
||||||
|
|
||||||
|
/// 0x3 : AF_UNIX : the forwarded connection uses the AF_UNIX address family
|
||||||
|
/// (UNIX). The addresses are exactly 108 bytes each.
|
||||||
|
Unix,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddressFamily {
|
||||||
|
fn v1_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
AddressFamily::Inet => "TCP4",
|
||||||
|
AddressFamily::Inet6 => "TCP6",
|
||||||
|
af => panic!("{:?} is not supported in PROXY v1", af),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn v2_hi(&self) -> u8 {
|
||||||
|
(match self {
|
||||||
|
AddressFamily::Unspecified => 0x0,
|
||||||
|
AddressFamily::Inet => 0x1,
|
||||||
|
AddressFamily::Inet6 => 0x2,
|
||||||
|
AddressFamily::Unix => 0x3,
|
||||||
|
}) << 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transport Protocol.
|
||||||
|
///
|
||||||
|
/// other values are unspecified and must not be emitted in version 2 of this
|
||||||
|
/// protocol and must be rejected as invalid by receivers.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum TransportProtocol {
|
||||||
|
/// 0x0 : UNSPEC : the connection is forwarded for an unknown, unspecified
|
||||||
|
/// or unsupported protocol. The sender should use this family when sending
|
||||||
|
/// LOCAL commands or when dealing with unsupported protocol families. The
|
||||||
|
/// receiver is free to accept the connection anyway and use the real endpoint
|
||||||
|
/// addresses or to reject it. The receiver should ignore address information.
|
||||||
|
Unspecified,
|
||||||
|
|
||||||
|
/// 0x1 : STREAM : the forwarded connection uses a SOCK_STREAM protocol (eg:
|
||||||
|
/// TCP or UNIX_STREAM). When used with AF_INET/AF_INET6 (TCP), the addresses
|
||||||
|
/// are followed by the source and destination ports represented on 2 bytes
|
||||||
|
/// each in network byte order.
|
||||||
|
Stream,
|
||||||
|
|
||||||
|
/// 0x2 : DGRAM : the forwarded connection uses a SOCK_DGRAM protocol (eg:
|
||||||
|
/// UDP or UNIX_DGRAM). When used with AF_INET/AF_INET6 (UDP), the addresses
|
||||||
|
/// are followed by the source and destination ports represented on 2 bytes
|
||||||
|
/// each in network byte order.
|
||||||
|
Datagram,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportProtocol {
|
||||||
|
const fn v2_lo(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
TransportProtocol::Unspecified => 0x0,
|
||||||
|
TransportProtocol::Stream => 0x1,
|
||||||
|
TransportProtocol::Datagram => 0x2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ProxyProtocolHeader {
|
||||||
|
V1(v1::Header),
|
||||||
|
V2(v2::Header),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct Pp2Crc32c {
|
||||||
|
checksum: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pp2Crc32c {
|
||||||
|
fn to_tlv(&self) -> Tlv {
|
||||||
|
Tlv {
|
||||||
|
r#type: 0x03,
|
||||||
|
value: self.checksum.to_be_bytes().to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Tlv {
|
||||||
|
r#type: u8,
|
||||||
|
value: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tlv {
|
||||||
|
pub fn new(r#type: u8, value: impl Into<Vec<u8>>) -> Self {
|
||||||
|
let value = value.into();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!value.is_empty(),
|
||||||
|
"TLV values must have length greater than 1",
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { r#type, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> u16 {
|
||||||
|
// 1b type + 2b len
|
||||||
|
// + [len]b value
|
||||||
|
1 + 2 + (self.value.len() as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to(&self, wrt: &mut impl io::Write) -> io::Result<()> {
|
||||||
|
wrt.write_all(&[self.r#type])?;
|
||||||
|
wrt.write_all(&(self.value.len() as u16).to_be_bytes())?;
|
||||||
|
wrt.write_all(&self.value)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_crc32c(&self) -> Option<Pp2Crc32c> {
|
||||||
|
if self.r#type != 0x03 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let checksum_bytes = <[u8; 4]>::try_from(self.value.as_slice()).ok()?;
|
||||||
|
|
||||||
|
Some(Pp2Crc32c {
|
||||||
|
checksum: u32::from_be_bytes(checksum_bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tlv_as_crc32c() {
|
||||||
|
let noop = Tlv::new(0x04, vec![0x00]);
|
||||||
|
assert_eq!(noop.as_crc32c(), None);
|
||||||
|
|
||||||
|
let noop = Tlv::new(0x03, vec![0x08, 0x70, 0x17, 0x7b]);
|
||||||
|
assert_eq!(
|
||||||
|
noop.as_crc32c(),
|
||||||
|
Some(Pp2Crc32c {
|
||||||
|
checksum: 141563771
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
actix-proxy-protocol/src/v1/mod.rs
Normal file
60
actix-proxy-protocol/src/v1/mod.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use std::{fmt, io, net::SocketAddr};
|
||||||
|
|
||||||
|
use arrayvec::ArrayVec;
|
||||||
|
use tokio::io::{AsyncWrite, AsyncWriteExt as _};
|
||||||
|
|
||||||
|
use crate::AddressFamily;
|
||||||
|
|
||||||
|
pub(crate) const SIGNATURE: &str = "PROXY";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Header {
|
||||||
|
/// Address family.
|
||||||
|
af: AddressFamily,
|
||||||
|
|
||||||
|
/// Source address.
|
||||||
|
src: SocketAddr,
|
||||||
|
|
||||||
|
/// Destination address.
|
||||||
|
dst: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
pub const fn new(af: AddressFamily, src: SocketAddr, dst: SocketAddr) -> Self {
|
||||||
|
Self { af, src, dst }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn new_inet(src: SocketAddr, dst: SocketAddr) -> Self {
|
||||||
|
Self::new(AddressFamily::Inet, src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn new_inet6(src: SocketAddr, dst: SocketAddr) -> Self {
|
||||||
|
Self::new(AddressFamily::Inet6, src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to(&self, wrt: &mut impl io::Write) -> io::Result<()> {
|
||||||
|
write!(wrt, "{}", self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_to_tokio(&self, wrt: &mut (impl AsyncWrite + Unpin)) -> io::Result<()> {
|
||||||
|
// max length of a V1 header is 107 bytes
|
||||||
|
let mut buf = ArrayVec::<_, 107>::new();
|
||||||
|
self.write_to(&mut buf)?;
|
||||||
|
wrt.write_all(&buf).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Header {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{proto_sig} {af} {src_ip} {dst_ip} {src_port} {dst_port}\r\n",
|
||||||
|
proto_sig = SIGNATURE,
|
||||||
|
af = self.af.v1_str(),
|
||||||
|
src_ip = self.src.ip(),
|
||||||
|
dst_ip = self.dst.ip(),
|
||||||
|
src_port = itoa::Buffer::new().format(self.src.port()),
|
||||||
|
dst_port = itoa::Buffer::new().format(self.dst.port()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
272
actix-proxy-protocol/src/v2/mod.rs
Normal file
272
actix-proxy-protocol/src/v2/mod.rs
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
net::{IpAddr, SocketAddr},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::io::{AsyncWrite, AsyncWriteExt as _};
|
||||||
|
|
||||||
|
use crate::{AddressFamily, Command, Pp2Crc32c, Tlv, TransportProtocol, Version};
|
||||||
|
|
||||||
|
pub(crate) const SIGNATURE: [u8; 12] = [
|
||||||
|
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Header {
|
||||||
|
command: Command,
|
||||||
|
transport_protocol: TransportProtocol,
|
||||||
|
address_family: AddressFamily,
|
||||||
|
src: SocketAddr,
|
||||||
|
dst: SocketAddr,
|
||||||
|
tlvs: Vec<Tlv>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
pub const fn new(
|
||||||
|
command: Command,
|
||||||
|
transport_protocol: TransportProtocol,
|
||||||
|
address_family: AddressFamily,
|
||||||
|
src: SocketAddr,
|
||||||
|
dst: SocketAddr,
|
||||||
|
tlvs: Vec<Tlv>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
command,
|
||||||
|
transport_protocol,
|
||||||
|
address_family,
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
tlvs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v2_len(&self) -> u16 {
|
||||||
|
let addr_len = if self.src.is_ipv4() {
|
||||||
|
4 + 2 // 4b IPv4 + 2b port number
|
||||||
|
} else {
|
||||||
|
16 + 2 // 16b IPv6 + 2b port number
|
||||||
|
};
|
||||||
|
|
||||||
|
(addr_len * 2) + self.tlvs.iter().map(|tlv| tlv.len()).sum::<u16>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to(&self, wrt: &mut impl io::Write) -> io::Result<()> {
|
||||||
|
// PROXY v2 signature
|
||||||
|
wrt.write_all(&SIGNATURE)?;
|
||||||
|
|
||||||
|
// version | command
|
||||||
|
wrt.write_all(&[Version::V2.v2_hi() | self.command.v2_lo()])?;
|
||||||
|
|
||||||
|
// address family | transport protocol
|
||||||
|
wrt.write_all(&[self.address_family.v2_hi() | self.transport_protocol.v2_lo()])?;
|
||||||
|
|
||||||
|
// rest-of-header length
|
||||||
|
wrt.write_all(&self.v2_len().to_be_bytes())?;
|
||||||
|
|
||||||
|
tracing::debug!("proxy rest-of-header len: {}", self.v2_len());
|
||||||
|
|
||||||
|
fn write_ip_bytes_to(wrt: &mut impl io::Write, ip: IpAddr) -> io::Result<()> {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ip) => wrt.write_all(&ip.octets()),
|
||||||
|
IpAddr::V6(ip) => wrt.write_all(&ip.octets()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// L3 (IP) address
|
||||||
|
write_ip_bytes_to(wrt, self.src.ip())?;
|
||||||
|
write_ip_bytes_to(wrt, self.dst.ip())?;
|
||||||
|
|
||||||
|
// L4 ports
|
||||||
|
wrt.write_all(&self.src.port().to_be_bytes())?;
|
||||||
|
wrt.write_all(&self.dst.port().to_be_bytes())?;
|
||||||
|
|
||||||
|
// TLVs
|
||||||
|
for tlv in &self.tlvs {
|
||||||
|
tlv.write_to(wrt)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_to_tokio(&self, wrt: &mut (impl AsyncWrite + Unpin)) -> io::Result<()> {
|
||||||
|
let buf = self.to_vec();
|
||||||
|
wrt.write_all(&buf).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_vec(&self) -> Vec<u8> {
|
||||||
|
// TODO: figure out cap
|
||||||
|
let mut buf = Vec::with_capacity(64);
|
||||||
|
self.write_to(&mut buf).unwrap();
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_crc23c_checksum_tlv(&mut self) {
|
||||||
|
if self.tlvs.iter().any(|tlv| tlv.as_crc32c().is_some()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the checksum is supported by the sender after constructing the header
|
||||||
|
// the sender MUST:
|
||||||
|
// - initialize the checksum field to '0's.
|
||||||
|
// - calculate the CRC32c checksum of the PROXY header as described in RFC4960,
|
||||||
|
// Appendix B [8].
|
||||||
|
// - put the resultant value into the checksum field, and leave the rest of
|
||||||
|
// the bits unchanged.
|
||||||
|
|
||||||
|
// add zeroed checksum field to TLVs
|
||||||
|
let crc = Pp2Crc32c::default().to_tlv();
|
||||||
|
self.tlvs.push(crc);
|
||||||
|
|
||||||
|
// write PROXY header to buffer
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
self.write_to(&mut buf).unwrap();
|
||||||
|
|
||||||
|
// calculate CRC on buffer and update CRC TLV
|
||||||
|
let crc_calc = crc32fast::hash(&buf);
|
||||||
|
self.tlvs.last_mut().unwrap().value = crc_calc.to_be_bytes().to_vec();
|
||||||
|
|
||||||
|
tracing::debug!("checksum is {}", crc_calc);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_crc32c_tlv(&self) -> Option<bool> {
|
||||||
|
dbg!(&self.tlvs);
|
||||||
|
|
||||||
|
// exit early if no crc32c TLV is present
|
||||||
|
let crc_sent = self.tlvs.iter().filter_map(|tlv| tlv.as_crc32c()).next()?;
|
||||||
|
|
||||||
|
// If the checksum is provided as part of the PROXY header and the checksum
|
||||||
|
// functionality is supported by the receiver, the receiver MUST:
|
||||||
|
// - store the received CRC32c checksum value aside.
|
||||||
|
// - replace the 32 bits of the checksum field in the received PROXY header with
|
||||||
|
// all '0's and calculate a CRC32c checksum value of the whole PROXY header.
|
||||||
|
// - verify that the calculated CRC32c checksum is the same as the received
|
||||||
|
// CRC32c checksum. If it is not, the receiver MUST treat the TCP connection
|
||||||
|
// providing the header as invalid.
|
||||||
|
// The default procedure for handling an invalid TCP connection is to abort it.
|
||||||
|
|
||||||
|
let mut this = self.clone();
|
||||||
|
for tlv in this.tlvs.iter_mut() {
|
||||||
|
if tlv.as_crc32c().is_some() {
|
||||||
|
tlv.value.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
this.write_to(&mut buf).unwrap();
|
||||||
|
let mut crc_calc = crc32fast::hash(&buf);
|
||||||
|
|
||||||
|
Some(crc_sent.checksum == crc_calc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::net::Ipv6Addr;
|
||||||
|
|
||||||
|
use const_str::hex;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn tlv_zero_len() {
|
||||||
|
Tlv::new(0x00, vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_v2_no_tlvs() {
|
||||||
|
let mut exp = Vec::new();
|
||||||
|
exp.extend_from_slice(&SIGNATURE); // 0-11
|
||||||
|
exp.extend_from_slice(&[0x21, 0x11]); // 12-13
|
||||||
|
exp.extend_from_slice(&[0x00, 0x0C]); // 14-15
|
||||||
|
exp.extend_from_slice(&[127, 0, 0, 1, 127, 0, 0, 2]); // 16-23
|
||||||
|
exp.extend_from_slice(&[0x04, 0xd2, 0x00, 80]); // 24-27
|
||||||
|
|
||||||
|
let header = Header::new(
|
||||||
|
Command::Proxy,
|
||||||
|
TransportProtocol::Stream,
|
||||||
|
AddressFamily::Inet,
|
||||||
|
SocketAddr::from(([127, 0, 0, 1], 1234)),
|
||||||
|
SocketAddr::from(([127, 0, 0, 2], 80)),
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(header.v2_len(), 12);
|
||||||
|
assert_eq!(header.to_vec(), exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_v2_ipv6_tlv_noop() {
|
||||||
|
let mut exp = Vec::new();
|
||||||
|
exp.extend_from_slice(&SIGNATURE); // 0-11
|
||||||
|
exp.extend_from_slice(&[0x20, 0x11]); // 12-13
|
||||||
|
exp.extend_from_slice(&[0x00, 0x28]); // 14-15
|
||||||
|
exp.extend_from_slice(&hex!("00000000000000000000000000000001")); // 16-31
|
||||||
|
exp.extend_from_slice(&hex!("000102030405060708090A0B0C0D0E0F")); // 32-45
|
||||||
|
exp.extend_from_slice(&[0x00, 80, 0xff, 0xff]); // 45-49
|
||||||
|
exp.extend_from_slice(&[0x04, 0x00, 0x01, 0x00]); // 50-53 NOOP TLV
|
||||||
|
|
||||||
|
let header = Header::new(
|
||||||
|
Command::Local,
|
||||||
|
TransportProtocol::Stream,
|
||||||
|
AddressFamily::Inet,
|
||||||
|
SocketAddr::from((Ipv6Addr::LOCALHOST, 80)),
|
||||||
|
SocketAddr::from((
|
||||||
|
Ipv6Addr::from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
|
||||||
|
65535,
|
||||||
|
)),
|
||||||
|
vec![Tlv::new(0x04, [0])],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(header.v2_len(), 36 + 4);
|
||||||
|
assert_eq!(header.to_vec(), exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_v2_tlv_c2c() {
|
||||||
|
let mut exp = Vec::new();
|
||||||
|
exp.extend_from_slice(&SIGNATURE); // 0-11
|
||||||
|
exp.extend_from_slice(&[0x21, 0x11]); // 12-13
|
||||||
|
exp.extend_from_slice(&[0x00, 0x13]); // 14-15
|
||||||
|
exp.extend_from_slice(&[127, 0, 0, 1, 127, 0, 0, 1]); // 16-23
|
||||||
|
exp.extend_from_slice(&[0x00, 80, 0x00, 80]); // 24-27
|
||||||
|
exp.extend_from_slice(&[0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); // 28-35 TLV crc32c
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
crc32fast::hash(&exp),
|
||||||
|
// correct checksum calculated manually
|
||||||
|
u32::from_be_bytes([0x08, 0x70, 0x17, 0x7b]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// re-assign actual checksum to last 4 bytes of expected byte array
|
||||||
|
exp[31..35].copy_from_slice(&[0x08, 0x70, 0x17, 0x7b]);
|
||||||
|
|
||||||
|
let mut header = Header::new(
|
||||||
|
Command::Proxy,
|
||||||
|
TransportProtocol::Stream,
|
||||||
|
AddressFamily::Inet,
|
||||||
|
SocketAddr::from(([127, 0, 0, 1], 80)),
|
||||||
|
SocketAddr::from(([127, 0, 0, 1], 80)),
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate_crc32c_tlv().is_none(),
|
||||||
|
"header doesn't have CRC TLV added yet"
|
||||||
|
);
|
||||||
|
|
||||||
|
// add crc32c TLV to header
|
||||||
|
header.add_crc23c_checksum_tlv();
|
||||||
|
|
||||||
|
assert_eq!(header.v2_len(), 12 + 7);
|
||||||
|
assert_eq!(header.to_vec(), exp);
|
||||||
|
|
||||||
|
// struct can self-validate checksum
|
||||||
|
assert_eq!(header.validate_crc32c_tlv().unwrap(), true);
|
||||||
|
|
||||||
|
// mangle crc32c TLV and assert that validate now fails
|
||||||
|
*header.tlvs.last_mut().unwrap().value.last_mut().unwrap() = 0x00;
|
||||||
|
assert_eq!(header.validate_crc32c_tlv().unwrap(), false);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user