mirror of
https://github.com/awfufu/traudit
synced 2026-03-01 05:29:44 +08:00
init: first commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
2333
Cargo.lock
generated
Normal file
2333
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "traudit"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["awfufu"]
|
||||||
|
description = "A reverse proxy with auditing capabilities."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "mysql", "postgres", "sqlite"] }
|
||||||
|
clickhouse = { version = "0.13", features = ["test-util"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
socket2 = "0.5"
|
||||||
|
libc = "0.2"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
anyhow = "1.0"
|
||||||
|
thiserror = "2.0"
|
||||||
|
bytes = "1.1"
|
||||||
|
url = "2.5"
|
||||||
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
37
README.md
Normal file
37
README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# traudit (Traffic Audit)
|
||||||
|
|
||||||
|
English | [简体中文](README_cn.md)
|
||||||
|
|
||||||
|
traudit is a reverse proxy supporting TCP/UDP/Unix Sockets, focused on connection auditing with support for multiple databases.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multi-Protocol Support**: TCP, UDP, Unix Domain Sockets.
|
||||||
|
- **Proxy Protocol**: Support Proxy Protocol to record real IP.
|
||||||
|
- **Audit Logging**: Store connection information in databases (ClickHouse, MySQL, PostgreSQL, SQLite).
|
||||||
|
- **Zero-Copy Forwarding**: Uses `splice` on Linux for zero-copy forwarding.
|
||||||
|
|
||||||
|
What? You don't need a database? Then go use [HAProxy](https://www.haproxy.org/).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
See [config_example.yaml](config_example.yaml).
|
||||||
|
|
||||||
|
## TODO List
|
||||||
|
|
||||||
|
- [ ] Core Implementation
|
||||||
|
- [ ] Configuration parsing (`serde_yaml`)
|
||||||
|
- [ ] TCP/UDP/Unix Listener abstraction
|
||||||
|
- [ ] Proxy Protocol parsing & handling
|
||||||
|
- [ ] Zero-copy forwarding loop (`splice`)
|
||||||
|
- [ ] Database Integration
|
||||||
|
- [ ] Define Audit Log schema
|
||||||
|
- [ ] Implement `AuditLogger` trait
|
||||||
|
- [ ] ClickHouse adapter
|
||||||
|
- [ ] SQLite/MySQL adapters
|
||||||
|
- [ ] Testing
|
||||||
|
- [ ] Unit tests for config & protocol
|
||||||
|
- [ ] End-to-end forwarding tests
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] Detailed configuration guide
|
||||||
|
- [ ] Deployment guide
|
||||||
37
README_cn.md
Normal file
37
README_cn.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# traudit (Traffic Audit)
|
||||||
|
|
||||||
|
[English](README.md) | 简体中文
|
||||||
|
|
||||||
|
traudit 是一个支持 TCP/UDP/Unix Socket 的反向代理程序,专注于连接审计,支持多种数据库。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- **多协议支持**: 支持 TCP, UDP, Unix Domain Socket。
|
||||||
|
- **Proxy Protocol**: 支持 Proxy Protocol 以记录真实 IP。
|
||||||
|
- **审计日志**: 将连接信息存入数据库 (ClickHouse, MySQL, PostgreSQL, SQLite)。
|
||||||
|
- **高性能转发**: 在 Linux 下使用 `splice` 实现零拷贝转发。
|
||||||
|
|
||||||
|
什么?你不需要数据库?那你去用 [HAProxy](https://www.haproxy.org/) 吧。
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
请查看 [config_example.yaml](config_example.yaml)。
|
||||||
|
|
||||||
|
## TODO List
|
||||||
|
|
||||||
|
- [ ] 核心功能
|
||||||
|
- [ ] 配置文件解析 (`serde_yaml`)
|
||||||
|
- [ ] 监听器抽象 (TCP/UDP/Unix)
|
||||||
|
- [ ] Proxy Protocol 解析与处理
|
||||||
|
- [ ] 零拷贝转发循环 (`splice`)
|
||||||
|
- [ ] 数据库
|
||||||
|
- [ ] 定义审计日志结构
|
||||||
|
- [ ] 实现 `AuditLogger` Trait
|
||||||
|
- [ ] ClickHouse 适配器
|
||||||
|
- [ ] SQLite/MySQL 适配器
|
||||||
|
- [ ] 测试
|
||||||
|
- [ ] 单元测试 (配置与协议)
|
||||||
|
- [ ] 端到端转发测试
|
||||||
|
- [ ] 文档
|
||||||
|
- [ ] 详细配置指南
|
||||||
|
- [ ] 部署文档
|
||||||
42
config_example.yaml
Normal file
42
config_example.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Traudit Configuration Example
|
||||||
|
|
||||||
|
database:
|
||||||
|
dsn: "clickhouse://admin:password@127.0.0.1:8123/audit_db"
|
||||||
|
batch:
|
||||||
|
size: 50
|
||||||
|
timeout_secs: 5
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Scenario: SSH High-Level Audit Gateway
|
||||||
|
# Receives traffic from FRP with v2 Proxy Protocol header, audits it,
|
||||||
|
# strips the header, and forwards pure TCP to local SSHD.
|
||||||
|
- name: "ssh-prod"
|
||||||
|
db_table: "ssh_audit_logs"
|
||||||
|
|
||||||
|
binds:
|
||||||
|
# Entry 1: Public traffic from FRP
|
||||||
|
- type: "tcp"
|
||||||
|
addr: "0.0.0.0:1222"
|
||||||
|
proxy_protocol: "v2"
|
||||||
|
|
||||||
|
# Entry 2: LAN direct traffic (no Proxy Protocol)
|
||||||
|
- type: "tcp"
|
||||||
|
addr: "0.0.0.0:1223"
|
||||||
|
|
||||||
|
forward_type: "tcp"
|
||||||
|
forward_addr: "127.0.0.1:22"
|
||||||
|
# forward_proxy_protocol omitted, sends pure stream to SSHD
|
||||||
|
|
||||||
|
# Scenario: Protocol Conversion and Local Socket Forwarding
|
||||||
|
# Receives normal TCP traffic, converts to v1 Proxy Protocol header,
|
||||||
|
# and forwards to local Unix socket (Nginx).
|
||||||
|
- name: "web-gateway"
|
||||||
|
db_table: "http_access_audit"
|
||||||
|
|
||||||
|
binds:
|
||||||
|
- type: "tcp"
|
||||||
|
addr: "0.0.0.0:8080"
|
||||||
|
|
||||||
|
forward_type: "unix"
|
||||||
|
forward_addr: "/run/nginx/web.sock"
|
||||||
|
forward_proxy_protocol: "v1"
|
||||||
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
tab_spaces = 2
|
||||||
|
edition = "2024"
|
||||||
116
src/config/mod.rs
Normal file
116
src/config/mod.rs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use std::path::Path;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub database: DatabaseConfig,
|
||||||
|
pub services: Vec<ServiceConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct DatabaseConfig {
|
||||||
|
pub dsn: String,
|
||||||
|
pub batch: BatchConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct BatchConfig {
|
||||||
|
pub size: usize,
|
||||||
|
pub timeout_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ServiceConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub db_table: String,
|
||||||
|
pub binds: Vec<BindConfig>,
|
||||||
|
pub forward_type: ForwardType,
|
||||||
|
pub forward_addr: String,
|
||||||
|
pub forward_proxy_protocol: Option<ProxyProtocolVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct BindConfig {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub bind_type: BindType,
|
||||||
|
pub addr: String,
|
||||||
|
pub proxy_protocol: Option<ProxyProtocolVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum BindType {
|
||||||
|
Tcp,
|
||||||
|
Udp,
|
||||||
|
Unix,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ForwardType {
|
||||||
|
Tcp,
|
||||||
|
Udp,
|
||||||
|
Unix,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ProxyProtocolVersion {
|
||||||
|
V1,
|
||||||
|
V2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub async fn load<P: AsRef<Path>>(path: P) -> Result<Self, anyhow::Error> {
|
||||||
|
let content = fs::read_to_string(path).await?;
|
||||||
|
let config: Config = serde_yaml::from_str(&content)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_load_config() {
|
||||||
|
let config_str = r#"
|
||||||
|
database:
|
||||||
|
dsn: "clickhouse://admin:password@127.0.0.1:8123/audit_db"
|
||||||
|
batch:
|
||||||
|
size: 50
|
||||||
|
timeout_secs: 5
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: "ssh-prod"
|
||||||
|
db_table: "ssh_audit_logs"
|
||||||
|
binds:
|
||||||
|
- type: "tcp"
|
||||||
|
addr: "0.0.0.0:22222"
|
||||||
|
proxy_protocol: "v2"
|
||||||
|
forward_type: "tcp"
|
||||||
|
forward_addr: "127.0.0.1:22"
|
||||||
|
"#;
|
||||||
|
let mut file = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
write!(file, "{}", config_str).unwrap();
|
||||||
|
let path = file.path().to_path_buf();
|
||||||
|
// Close the file handle so tokio can read it, or just keep it open and read by path?
|
||||||
|
// tempfile deletes on drop. We need to keep `file` alive.
|
||||||
|
|
||||||
|
let config = Config::load(&path).await.expect("Failed to load config");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.database.dsn,
|
||||||
|
"clickhouse://admin:password@127.0.0.1:8123/audit_db"
|
||||||
|
);
|
||||||
|
assert_eq!(config.services.len(), 1);
|
||||||
|
assert_eq!(config.services[0].name, "ssh-prod");
|
||||||
|
assert_eq!(config.services[0].binds[0].bind_type, BindType::Tcp);
|
||||||
|
assert_eq!(
|
||||||
|
config.services[0].binds[0].proxy_protocol,
|
||||||
|
Some(ProxyProtocolVersion::V2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/core/mod.rs
Normal file
1
src/core/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod server;
|
||||||
70
src/core/server.rs
Normal file
70
src/core/server.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use crate::config::{BindType, Config};
|
||||||
|
use crate::db::clickhouse::ClickHouseLogger;
|
||||||
|
use crate::db::AuditLogger;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio::signal;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
pub async fn run(config: Config) -> anyhow::Result<()> {
|
||||||
|
let db = Arc::new(ClickHouseLogger::new(&config.database));
|
||||||
|
|
||||||
|
let mut join_set = tokio::task::JoinSet::new();
|
||||||
|
|
||||||
|
for service in config.services {
|
||||||
|
let db = db.clone();
|
||||||
|
for bind in service.binds {
|
||||||
|
let service_name = service.name.clone();
|
||||||
|
let bind_addr = bind.addr.clone();
|
||||||
|
let bind_type = bind.bind_type;
|
||||||
|
|
||||||
|
// TODO: Handle UDP and Unix
|
||||||
|
if bind_type == BindType::Tcp {
|
||||||
|
let db = db.clone();
|
||||||
|
join_set.spawn(start_tcp_service(service_name, bind_addr, db));
|
||||||
|
} else {
|
||||||
|
info!("Skipping non-TCP bind for now: {:?}", bind_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Traudit started.");
|
||||||
|
|
||||||
|
match signal::ctrl_c().await {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Shutdown signal received.");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Unable to listen for shutdown signal: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort all tasks
|
||||||
|
join_set.shutdown().await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_tcp_service(name: String, addr: String, _db: Arc<ClickHouseLogger>) {
|
||||||
|
info!("Service {} listening on TCP {}", name, addr);
|
||||||
|
let listener = match TcpListener::bind(&addr).await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to bind {}: {}", addr, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match listener.accept().await {
|
||||||
|
Ok((_socket, client_addr)) => {
|
||||||
|
info!("New connection from {}", client_addr);
|
||||||
|
// Spawn handler
|
||||||
|
// tokio::spawn(handle_connection(_socket, ...));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Accept error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/db/clickhouse.rs
Normal file
20
src/db/clickhouse.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use super::{AuditEvent, AuditLogger};
|
||||||
|
use crate::config::DatabaseConfig;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
pub struct ClickHouseLogger;
|
||||||
|
|
||||||
|
impl ClickHouseLogger {
|
||||||
|
pub fn new(_config: &DatabaseConfig) -> Self {
|
||||||
|
// TODO: Initialize ClickHouse client
|
||||||
|
ClickHouseLogger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AuditLogger for ClickHouseLogger {
|
||||||
|
async fn log(&self, _event: AuditEvent) -> anyhow::Result<()> {
|
||||||
|
// TODO: Implement insertion logic
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/db/mod.rs
Normal file
14
src/db/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
pub mod clickhouse;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AuditEvent {
|
||||||
|
// TODO: Define audit event fields (src, dst, timestamp, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AuditLogger: Send + Sync {
|
||||||
|
// TODO: Finalize log interface
|
||||||
|
async fn log(&self, event: AuditEvent) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
28
src/main.rs
Normal file
28
src/main.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
mod config;
|
||||||
|
mod core;
|
||||||
|
mod db;
|
||||||
|
mod protocol;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let config_path = args.get(1).map(|s| s.as_str()).unwrap_or("config.yaml");
|
||||||
|
|
||||||
|
println!("Loading config from {}", config_path);
|
||||||
|
|
||||||
|
// Check if config exists, if not warn (for dev purposes)
|
||||||
|
if !std::path::Path::new(config_path).exists() {
|
||||||
|
println!("Warning: Config file '{}' not found.", config_path);
|
||||||
|
// In a real run we might want to exit, but for init check we proceed or return
|
||||||
|
} else {
|
||||||
|
let config = Config::load(config_path).await?;
|
||||||
|
core::server::run(config).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
203
src/protocol/mod.rs
Normal file
203
src/protocol/mod.rs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
use bytes::{Buf, BytesMut};
|
||||||
|
use std::io;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProxyInfo {
|
||||||
|
pub version: Version,
|
||||||
|
pub source: SocketAddr,
|
||||||
|
pub destination: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Version {
|
||||||
|
V1,
|
||||||
|
V2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const V1_PREFIX: &[u8] = b"PROXY ";
|
||||||
|
const V2_PREFIX: &[u8] = b"\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; // 12 bytes
|
||||||
|
|
||||||
|
pub async fn read_proxy_header<T: AsyncRead + Unpin>(
|
||||||
|
stream: &mut T,
|
||||||
|
) -> io::Result<(Option<ProxyInfo>, BytesMut)> {
|
||||||
|
let mut buf = BytesMut::with_capacity(512);
|
||||||
|
|
||||||
|
// Read enough to distinguish version
|
||||||
|
|
||||||
|
// Initial read
|
||||||
|
let n = stream.read_buf(&mut buf).await?;
|
||||||
|
if n == 0 {
|
||||||
|
return Ok((None, buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check V2
|
||||||
|
if buf.len() >= 12 && &buf[..12] == V2_PREFIX {
|
||||||
|
// v2
|
||||||
|
return parse_v2(stream, buf).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check V1
|
||||||
|
if buf.len() >= 6 && &buf[..6] == V1_PREFIX {
|
||||||
|
// v1
|
||||||
|
return parse_v1(stream, buf).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neither
|
||||||
|
Ok((None, buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_v1<T: AsyncRead + Unpin>(
|
||||||
|
stream: &mut T,
|
||||||
|
mut buf: BytesMut,
|
||||||
|
) -> io::Result<(Option<ProxyInfo>, BytesMut)> {
|
||||||
|
// Read line until \r\n
|
||||||
|
loop {
|
||||||
|
if let Some(pos) = buf.windows(2).position(|w| w == b"\r\n") {
|
||||||
|
// Found line end
|
||||||
|
let line_bytes = buf.split_to(pos + 2); // Consumes line including \r\n
|
||||||
|
let line = String::from_utf8_lossy(&line_bytes[..line_bytes.len() - 2]); // drop \r\n
|
||||||
|
|
||||||
|
let parts: Vec<&str> = line.split(' ').collect();
|
||||||
|
// PROXY TCP4 1.2.3.4 5.6.7.8 80 8080
|
||||||
|
if parts.len() >= 6 && parts[0] == "PROXY" {
|
||||||
|
let src_ip: IpAddr = parts[2]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid src IP"))?;
|
||||||
|
let dst_ip: IpAddr = parts[3]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid dst IP"))?;
|
||||||
|
let src_port: u16 = parts[4]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid src port"))?;
|
||||||
|
let dst_port: u16 = parts[5]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid dst port"))?;
|
||||||
|
|
||||||
|
return Ok((
|
||||||
|
Some(ProxyInfo {
|
||||||
|
version: Version::V1,
|
||||||
|
source: SocketAddr::new(src_ip, src_port),
|
||||||
|
destination: SocketAddr::new(dst_ip, dst_port),
|
||||||
|
}),
|
||||||
|
buf,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Ok((None, buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read more
|
||||||
|
let n = stream.read_buf(&mut buf).await?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::UnexpectedEof,
|
||||||
|
"Incomplete V1 header",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if buf.len() > 256 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"V1 header too too long",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_v2<T: AsyncRead + Unpin>(
|
||||||
|
stream: &mut T,
|
||||||
|
mut buf: BytesMut,
|
||||||
|
) -> io::Result<(Option<ProxyInfo>, BytesMut)> {
|
||||||
|
// We already have at least 12 bytes.
|
||||||
|
// 13th byte: ver_cmd (version 4 bits + command 4 bits)
|
||||||
|
// 14th byte: fam (family 4 bits + proto 4 bits)
|
||||||
|
// 15th-16th: len (u16 big endian)
|
||||||
|
|
||||||
|
while buf.len() < 16 {
|
||||||
|
let n = stream.read_buf(&mut buf).await?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::UnexpectedEof,
|
||||||
|
"Incomplete V2 header",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version (should be 2) and command (0=Local, 1=Proxy)
|
||||||
|
let ver_cmd = buf[12];
|
||||||
|
if (ver_cmd >> 4) != 2 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"Invalid Proxy Protocol version",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length
|
||||||
|
let len_bytes = [buf[14], buf[15]];
|
||||||
|
let len = u16::from_be_bytes(len_bytes) as usize;
|
||||||
|
|
||||||
|
// Read payload
|
||||||
|
while buf.len() < 16 + len {
|
||||||
|
let n = stream.read_buf(&mut buf).await?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::UnexpectedEof,
|
||||||
|
"Incomplete V2 payload",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume header + payload
|
||||||
|
let full_len = 16 + len;
|
||||||
|
let header_bytes = buf.split_to(full_len); // Now buf has remaining data
|
||||||
|
|
||||||
|
// Parse addresses from payload (header_bytes[16..])
|
||||||
|
let fam = header_bytes[13];
|
||||||
|
let payload = &header_bytes[16..];
|
||||||
|
|
||||||
|
match fam {
|
||||||
|
0x11 | 0x12 => {
|
||||||
|
// TCP/UDP over IPv4
|
||||||
|
if payload.len() >= 12 {
|
||||||
|
let src_ip = Ipv4Addr::new(payload[0], payload[1], payload[2], payload[3]);
|
||||||
|
let dst_ip = Ipv4Addr::new(payload[4], payload[5], payload[6], payload[7]);
|
||||||
|
let src_port = u16::from_be_bytes([payload[8], payload[9]]);
|
||||||
|
let dst_port = u16::from_be_bytes([payload[10], payload[11]]);
|
||||||
|
return Ok((
|
||||||
|
Some(ProxyInfo {
|
||||||
|
version: Version::V2,
|
||||||
|
source: SocketAddr::new(IpAddr::V4(src_ip), src_port),
|
||||||
|
destination: SocketAddr::new(IpAddr::V4(dst_ip), dst_port),
|
||||||
|
}),
|
||||||
|
buf,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x21 | 0x22 => {
|
||||||
|
// TCP/UDP over IPv6
|
||||||
|
if payload.len() >= 36 {
|
||||||
|
// IPv6 parsing...
|
||||||
|
// Keep it brief
|
||||||
|
let mut src = [0u8; 16];
|
||||||
|
src.copy_from_slice(&payload[0..16]);
|
||||||
|
let mut dst = [0u8; 16];
|
||||||
|
dst.copy_from_slice(&payload[16..32]);
|
||||||
|
let src_port = u16::from_be_bytes([payload[32], payload[33]]);
|
||||||
|
let dst_port = u16::from_be_bytes([payload[34], payload[35]]);
|
||||||
|
return Ok((
|
||||||
|
Some(ProxyInfo {
|
||||||
|
version: Version::V2,
|
||||||
|
source: SocketAddr::new(IpAddr::V6(Ipv6Addr::from(src)), src_port),
|
||||||
|
destination: SocketAddr::new(IpAddr::V6(Ipv6Addr::from(dst)), dst_port),
|
||||||
|
}),
|
||||||
|
buf,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If unsupported family or LOCAL command, return Info with dummy/empty or just ignore
|
||||||
|
// For now, if we can't parse addr, we return None but consume header.
|
||||||
|
Ok((None, buf))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user