wip(blaze-proto): retained Blaze frame/codec refinement WIP

RETAINED PRE-EXISTING WIP (brought forward after verification, not authored
here, not production-ready). Blaze wire frame/packet layout work (Fire2
header still flagged as an unvalidated FIFA23 guess). No secrets/captures
staged (captures/ + certs/ gitignored). Preserved off detached base f4f33969f2.
This commit is contained in:
funman300
2026-08-20 16:08:36 +00:00
parent f4f33969f2
commit b7126b255f
9 changed files with 227 additions and 144 deletions
+24 -11
View File
@@ -12,7 +12,7 @@ use std::io;
use tokio_util::codec::{Decoder, Encoder}; use tokio_util::codec::{Decoder, Encoder};
use super::{ use super::{
frame::{FireFrame, FIRE2_MIN_HEADER, FIRE2_JUMBO_EXT, PacketOptions}, frame::{FireFrame, PacketOptions, FIRE2_JUMBO_EXT, FIRE2_MIN_HEADER},
packet::Packet, packet::Packet,
}; };
@@ -31,8 +31,8 @@ pub enum FramingVariant {
/// Tracks partial-decode state between `decode()` calls. /// Tracks partial-decode state between `decode()` calls.
struct PartialFire2 { struct PartialFire2 {
frame : FireFrame, frame: FireFrame,
body_len : usize, body_len: usize,
} }
/// Tokio codec for encoding and decoding Blaze packets. /// Tokio codec for encoding and decoding Blaze packets.
@@ -44,18 +44,23 @@ pub struct PacketCodec {
impl PacketCodec { impl PacketCodec {
pub fn new(variant: FramingVariant) -> Self { pub fn new(variant: FramingVariant) -> Self {
PacketCodec { variant, partial: None } PacketCodec {
variant,
partial: None,
}
} }
} }
impl Decoder for PacketCodec { impl Decoder for PacketCodec {
type Item = Packet; type Item = Packet;
type Error = io::Error; type Error = io::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> { fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
match self.variant { match self.variant {
FramingVariant::Raw => { FramingVariant::Raw => {
if src.is_empty() { return Ok(None); } if src.is_empty() {
return Ok(None);
}
let body = src.copy_to_bytes(src.len()); let body = src.copy_to_bytes(src.len());
Ok(Some(Packet::raw(body))) Ok(Some(Packet::raw(body)))
} }
@@ -63,21 +68,29 @@ impl Decoder for PacketCodec {
FramingVariant::Fire2 => { FramingVariant::Fire2 => {
// ── Resume waiting for the body ─────────────────────────── // ── Resume waiting for the body ───────────────────────────
if let Some(ref p) = self.partial { if let Some(ref p) = self.partial {
if src.len() < p.body_len { return Ok(None); } if src.len() < p.body_len {
let PartialFire2 { frame, body_len, .. } = self.partial.take().unwrap(); return Ok(None);
}
let PartialFire2 {
frame, body_len, ..
} = self.partial.take().unwrap();
let body = src.copy_to_bytes(body_len); let body = src.copy_to_bytes(body_len);
return Ok(Some(Packet { frame, body })); return Ok(Some(Packet { frame, body }));
} }
// ── Need at least the minimum header ───────────────────── // ── Need at least the minimum header ─────────────────────
if src.len() < FIRE2_MIN_HEADER { return Ok(None); } if src.len() < FIRE2_MIN_HEADER {
return Ok(None);
}
// Peek at options to see if we need a jumbo ext too. // Peek at options to see if we need a jumbo ext too.
let opt_nibble = (src[9] >> 4) & 0xF; let opt_nibble = (src[9] >> 4) & 0xF;
let has_jumbo = (opt_nibble & PacketOptions::JUMBO_FRAME.0) != 0; let has_jumbo = (opt_nibble & PacketOptions::JUMBO_FRAME.0) != 0;
let header_len = FIRE2_MIN_HEADER + if has_jumbo { FIRE2_JUMBO_EXT } else { 0 }; let header_len = FIRE2_MIN_HEADER + if has_jumbo { FIRE2_JUMBO_EXT } else { 0 };
if src.len() < header_len { return Ok(None); } if src.len() < header_len {
return Ok(None);
}
let (frame, body_len) = FireFrame::read(src); let (frame, body_len) = FireFrame::read(src);
+51 -30
View File
@@ -52,10 +52,10 @@ pub const FIRE2_JUMBO_EXT: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)] #[repr(u8)]
pub enum FrameType { pub enum FrameType {
Request = 0x0, Request = 0x0,
Response = 0x1, Response = 0x1,
Notify = 0x2, Notify = 0x2,
Error = 0x3, Error = 0x3,
} }
impl From<u8> for FrameType { impl From<u8> for FrameType {
@@ -64,7 +64,7 @@ impl From<u8> for FrameType {
0x1 => FrameType::Response, 0x1 => FrameType::Response,
0x2 => FrameType::Notify, 0x2 => FrameType::Notify,
0x3 => FrameType::Error, 0x3 => FrameType::Error,
_ => FrameType::Request, _ => FrameType::Request,
} }
} }
} }
@@ -72,10 +72,10 @@ impl From<u8> for FrameType {
impl std::fmt::Display for FrameType { impl std::fmt::Display for FrameType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
FrameType::Request => write!(f, "REQUEST"), FrameType::Request => write!(f, "REQUEST"),
FrameType::Response => write!(f, "RESPONSE"), FrameType::Response => write!(f, "RESPONSE"),
FrameType::Notify => write!(f, "NOTIFY"), FrameType::Notify => write!(f, "NOTIFY"),
FrameType::Error => write!(f, "ERROR"), FrameType::Error => write!(f, "ERROR"),
} }
} }
} }
@@ -85,22 +85,24 @@ impl std::fmt::Display for FrameType {
pub struct PacketOptions(pub u8); pub struct PacketOptions(pub u8);
impl PacketOptions { impl PacketOptions {
pub const NONE : Self = Self(0x0); pub const NONE: Self = Self(0x0);
/// Body length exceeds 16 bits; an extra u16 follows the header. /// Body length exceeds 16 bits; an extra u16 follows the header.
pub const JUMBO_FRAME: Self = Self(0x1); pub const JUMBO_FRAME: Self = Self(0x1);
pub fn contains(self, flag: Self) -> bool { (self.0 & flag.0) != 0 } pub fn contains(self, flag: Self) -> bool {
(self.0 & flag.0) != 0
}
} }
/// Fire2 packet header. /// Fire2 packet header.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FireFrame { pub struct FireFrame {
pub component : u16, pub component: u16,
pub command : u16, pub command: u16,
pub error : u16, pub error: u16,
pub ty : FrameType, pub ty: FrameType,
pub options : PacketOptions, pub options: PacketOptions,
pub seq : u16, pub seq: u16,
} }
impl FireFrame { impl FireFrame {
@@ -108,23 +110,32 @@ impl FireFrame {
pub fn response(&self) -> Self { pub fn response(&self) -> Self {
Self { Self {
component: self.component, component: self.component,
command: self.command, command: self.command,
error: 0, error: 0,
ty: FrameType::Response, ty: FrameType::Response,
options: PacketOptions::NONE, options: PacketOptions::NONE,
seq: self.seq, seq: self.seq,
} }
} }
/// Build a notify frame. /// Build a notify frame.
pub fn notify(component: u16, command: u16) -> Self { pub fn notify(component: u16, command: u16) -> Self {
Self { component, command, error: 0, ty: FrameType::Notify, options: PacketOptions::NONE, seq: 0 } Self {
component,
command,
error: 0,
ty: FrameType::Notify,
options: PacketOptions::NONE,
seq: 0,
}
} }
/// Encode this header into `dst`, prepending `body_len` at the front. /// Encode this header into `dst`, prepending `body_len` at the front.
pub fn write(&self, dst: &mut BytesMut, body_len: usize) { pub fn write(&self, dst: &mut BytesMut, body_len: usize) {
let mut options = self.options; let mut options = self.options;
if body_len > u16::MAX as usize { options = PacketOptions(options.0 | PacketOptions::JUMBO_FRAME.0); } if body_len > u16::MAX as usize {
options = PacketOptions(options.0 | PacketOptions::JUMBO_FRAME.0);
}
dst.put_u16(body_len as u16); dst.put_u16(body_len as u16);
dst.put_u16(self.component); dst.put_u16(self.component);
@@ -142,15 +153,15 @@ impl FireFrame {
/// Parse a Fire2 header from `src`, which must already have `>= FIRE2_MIN_HEADER` bytes. /// Parse a Fire2 header from `src`, which must already have `>= FIRE2_MIN_HEADER` bytes.
/// Returns `(frame, body_length)`. Advances `src` past the header (including jumbo ext). /// Returns `(frame, body_length)`. Advances `src` past the header (including jumbo ext).
pub fn read(src: &mut BytesMut) -> (FireFrame, usize) { pub fn read(src: &mut BytesMut) -> (FireFrame, usize) {
let len_lo = src.get_u16() as usize; let len_lo = src.get_u16() as usize;
let component = src.get_u16(); let component = src.get_u16();
let command = src.get_u16(); let command = src.get_u16();
let error = src.get_u16(); let error = src.get_u16();
let ty_byte = src.get_u8(); let ty_byte = src.get_u8();
let opt_byte = src.get_u8(); let opt_byte = src.get_u8();
let seq = src.get_u16(); let seq = src.get_u16();
let ty = FrameType::from(ty_byte >> 4); let ty = FrameType::from(ty_byte >> 4);
let options = PacketOptions(opt_byte >> 4); let options = PacketOptions(opt_byte >> 4);
let body_len = if options.contains(PacketOptions::JUMBO_FRAME) { let body_len = if options.contains(PacketOptions::JUMBO_FRAME) {
@@ -160,6 +171,16 @@ impl FireFrame {
len_lo len_lo
}; };
(FireFrame { component, command, error, ty, options, seq }, body_len) (
FireFrame {
component,
command,
error,
ty,
options,
seq,
},
body_len,
)
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
pub mod frame;
pub mod codec; pub mod codec;
pub mod frame;
pub mod packet; pub mod packet;
pub use codec::{FramingVariant, PacketCodec};
pub use frame::{FireFrame, FrameType, PacketOptions}; pub use frame::{FireFrame, FrameType, PacketOptions};
pub use codec::{PacketCodec, FramingVariant};
pub use packet::Packet; pub use packet::Packet;
+25 -22
View File
@@ -2,8 +2,8 @@ use bytes::Bytes;
use serde::Serialize; use serde::Serialize;
use super::frame::{FireFrame, FrameType, PacketOptions}; use super::frame::{FireFrame, FrameType, PacketOptions};
use tdf::stringify::TdfStringifier;
use tdf::reader::TdfDeserializer; use tdf::reader::TdfDeserializer;
use tdf::stringify::TdfStringifier;
/// A fully framed Blaze packet. /// A fully framed Blaze packet.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -20,11 +20,11 @@ impl Packet {
Packet { Packet {
frame: FireFrame { frame: FireFrame {
component: 0, component: 0,
command: 0, command: 0,
error: 0, error: 0,
ty: FrameType::Request, ty: FrameType::Request,
options: PacketOptions::NONE, options: PacketOptions::NONE,
seq: 0, seq: 0,
}, },
body, body,
} }
@@ -32,14 +32,17 @@ impl Packet {
/// Build an empty response that mirrors `req`'s routing fields. /// Build an empty response that mirrors `req`'s routing fields.
pub fn response_empty(req: &Packet) -> Self { pub fn response_empty(req: &Packet) -> Self {
Packet { frame: req.frame.response(), body: Bytes::new() } Packet {
frame: req.frame.response(),
body: Bytes::new(),
}
} }
/// Build a response with the given TDF body. /// Build a response with the given TDF body.
pub fn response<V: tdf::TdfSerialize>(req: &Packet, value: &V) -> Self { pub fn response<V: tdf::TdfSerialize>(req: &Packet, value: &V) -> Self {
Packet { Packet {
frame: req.frame.response(), frame: req.frame.response(),
body: Bytes::from(tdf::serialize_vec(value)), body: Bytes::from(tdf::serialize_vec(value)),
} }
} }
@@ -62,13 +65,13 @@ impl Packet {
pub fn to_capture(&self) -> CaptureRecord { pub fn to_capture(&self) -> CaptureRecord {
CaptureRecord { CaptureRecord {
component: self.frame.component, component: self.frame.component,
command: self.frame.command, command: self.frame.command,
error: self.frame.error, error: self.frame.error,
ty: self.frame.ty.to_string(), ty: self.frame.ty.to_string(),
seq: self.frame.seq, seq: self.frame.seq,
body_len: self.body.len(), body_len: self.body.len(),
raw_hex: hex::encode(&self.body), raw_hex: hex::encode(&self.body),
tdf: self.tdf_string(), tdf: self.tdf_string(),
} }
} }
} }
@@ -76,11 +79,11 @@ impl Packet {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct CaptureRecord { pub struct CaptureRecord {
pub component: u16, pub component: u16,
pub command: u16, pub command: u16,
pub error: u16, pub error: u16,
pub ty: String, pub ty: String,
pub seq: u16, pub seq: u16,
pub body_len: usize, pub body_len: usize,
pub raw_hex: String, pub raw_hex: String,
pub tdf: String, pub tdf: String,
} }
+51 -25
View File
@@ -3,45 +3,57 @@
//! Every packet (in and out) is appended to a JSONL file so the session can //! Every packet (in and out) is appended to a JSONL file so the session can
//! be replayed with `jq` and the component/command IDs can be catalogued. //! be replayed with `jq` and the component/command IDs can be catalogued.
use blaze_proto::Packet;
use chrono::Utc;
use serde_json::json;
use std::{ use std::{
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::Write, io::Write,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use blaze_proto::Packet;
use chrono::Utc;
use serde_json::json;
use tracing::{info, warn}; use tracing::{info, warn};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Dir { In, Out } pub enum Dir {
In,
Out,
}
impl Dir { impl Dir {
fn as_str(self) -> &'static str { match self { Dir::In => "IN", Dir::Out => "OUT" } } fn as_str(self) -> &'static str {
match self {
Dir::In => "IN",
Dir::Out => "OUT",
}
}
} }
pub struct CaptureWriter { pub struct CaptureWriter {
file : Arc<Mutex<File>>, file: Arc<Mutex<File>>,
pretty : bool, pretty: bool,
counter : Arc<Mutex<u64>>, counter: Arc<Mutex<u64>>,
} }
impl CaptureWriter { impl CaptureWriter {
pub fn open(dir: &str, pretty: bool) -> anyhow::Result<Self> { pub fn open(dir: &str, pretty: bool) -> anyhow::Result<Self> {
std::fs::create_dir_all(dir)?; std::fs::create_dir_all(dir)?;
let ts = Utc::now().format("%Y%m%d-%H%M%S"); let ts = Utc::now().format("%Y%m%d-%H%M%S");
let path = format!("{dir}/capture-{ts}.jsonl"); let path = format!("{dir}/capture-{ts}.jsonl");
let file = OpenOptions::new().create(true).append(true).open(&path)?; let file = OpenOptions::new().create(true).append(true).open(&path)?;
info!(path, "capture file opened"); info!(path, "capture file opened");
Ok(CaptureWriter { Ok(CaptureWriter {
file: Arc::new(Mutex::new(file)), file: Arc::new(Mutex::new(file)),
pretty, pretty,
counter: Arc::new(Mutex::new(0)), counter: Arc::new(Mutex::new(0)),
}) })
} }
pub fn record(&self, pkt: &Packet, peer: &str, dir: Dir) { pub fn record(&self, pkt: &Packet, peer: &str, dir: Dir) {
let n = { let mut c = self.counter.lock().unwrap(); *c += 1; *c }; let n = {
let mut c = self.counter.lock().unwrap();
*c += 1;
*c
};
let ts = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); let ts = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let rec = pkt.to_capture(); let rec = pkt.to_capture();
@@ -67,21 +79,35 @@ impl CaptureWriter {
} }
if self.pretty { if self.pretty {
println!("\n╔══ #{n:04} {ts} {peer} {d} ══", println!("\n╔══ #{n:04} {ts} {peer} {d} ══", d = dir.as_str());
d = dir.as_str()); println!(
println!("║ component=0x{:04X} command=0x{:04X} {ty} seq={seq}", "║ component=0x{:04X} command=0x{:04X} {ty} seq={seq}",
rec.component, rec.command, ty = rec.ty, seq = rec.seq); rec.component,
rec.command,
ty = rec.ty,
seq = rec.seq
);
if rec.body_len > 0 { if rec.body_len > 0 {
println!("║ body ({} bytes):\n{}", println!(
rec.body_len, "║ body ({} bytes):\n{}",
pkt.body.chunks(16) rec.body_len,
.enumerate() pkt.body
.map(|(i, chunk)| { .chunks(16)
let h: String = chunk.iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "); .enumerate()
let a: String = chunk.iter().map(|&b| if b.is_ascii_graphic() { b as char } else { '.' }).collect(); .map(|(i, chunk)| {
format!("\n{:04X}: {h:<47} {a}", i * 16) let h: String = chunk
}) .iter()
.collect::<String>()); .map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(" ");
let a: String = chunk
.iter()
.map(|&b| if b.is_ascii_graphic() { b as char } else { '.' })
.collect();
format!("\n{:04X}: {h:<47} {a}", i * 16)
})
.collect::<String>()
);
println!("║ TDF: {}", rec.tdf); println!("║ TDF: {}", rec.tdf);
} }
println!("╚══════════════════════════════════════════════════"); println!("╚══════════════════════════════════════════════════");
+13 -14
View File
@@ -2,50 +2,49 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
pub redirector : RedirectorConfig, pub redirector: RedirectorConfig,
pub blaze : BlazeConfig, pub blaze: BlazeConfig,
pub tls : TlsConfig, pub tls: TlsConfig,
pub capture : CaptureConfig, pub capture: CaptureConfig,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct RedirectorConfig { pub struct RedirectorConfig {
/// Address to listen on. EA clients connect to gosredirector.ea.com which /// Address to listen on. EA clients connect to gosredirector.ea.com which
/// you redirect to 127.0.0.1 via /etc/hosts. /// you redirect to 127.0.0.1 via /etc/hosts.
pub listen : String, pub listen: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BlazeConfig { pub struct BlazeConfig {
/// Address the Blaze server listens on. /// Address the Blaze server listens on.
pub listen : String, pub listen: String,
/// Address / hostname we tell the client to connect to (in the redirect response). /// Address / hostname we tell the client to connect to (in the redirect response).
pub advertise_host : String, pub advertise_host: String,
/// Port we tell the client to connect to (in the redirect response). /// Port we tell the client to connect to (in the redirect response).
pub advertise_port : u16, pub advertise_port: u16,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct TlsConfig { pub struct TlsConfig {
/// PEM certificate file (RSA-2048 recommended — DirtySDK rejects ECDSA). /// PEM certificate file (RSA-2048 recommended — DirtySDK rejects ECDSA).
pub cert : String, pub cert: String,
/// PEM private key file. /// PEM private key file.
pub key : String, pub key: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CaptureConfig { pub struct CaptureConfig {
/// Directory where JSONL capture files are written. /// Directory where JSONL capture files are written.
pub dir : String, pub dir: String,
/// Also print decoded packet trees to stdout. /// Also print decoded packet trees to stdout.
pub pretty_print : bool, pub pretty_print: bool,
} }
impl Config { impl Config {
pub fn load(path: &str) -> anyhow::Result<Self> { pub fn load(path: &str) -> anyhow::Result<Self> {
let text = std::fs::read_to_string(path) let text = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("cannot read {path}: {e}"))?; .map_err(|e| anyhow::anyhow!("cannot read {path}: {e}"))?;
toml::from_str(&text) toml::from_str(&text).map_err(|e| anyhow::anyhow!("config parse error in {path}: {e}"))
.map_err(|e| anyhow::anyhow!("config parse error in {path}: {e}"))
} }
} }
+14 -8
View File
@@ -3,8 +3,8 @@
//! All FIFA 23 component/command IDs are UNKNOWN until capture reveals them. //! All FIFA 23 component/command IDs are UNKNOWN until capture reveals them.
//! The default handler returns an empty response — never panics, never hangs. //! The default handler returns an empty response — never panics, never hangs.
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
use blaze_proto::Packet; use blaze_proto::Packet;
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
use tracing::warn; use tracing::warn;
pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>; pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
@@ -15,15 +15,19 @@ pub struct Dispatcher {
} }
impl Dispatcher { impl Dispatcher {
pub fn new() -> Self { Dispatcher { table: HashMap::new() } } pub fn new() -> Self {
Dispatcher {
table: HashMap::new(),
}
}
pub fn register<F, Fut>(&mut self, component: u16, command: u16, f: F) pub fn register<F, Fut>(&mut self, component: u16, command: u16, f: F)
where where
F: Fn(Packet) -> Fut + Send + Sync + 'static, F: Fn(Packet) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Option<Packet>> + Send + 'static, Fut: Future<Output = Option<Packet>> + Send + 'static,
{ {
self.table.insert((component, command), self.table
Arc::new(move |p| Box::pin(f(p)))); .insert((component, command), Arc::new(move |p| Box::pin(f(p))));
} }
pub async fn dispatch(&self, pkt: Packet) -> Option<Packet> { pub async fn dispatch(&self, pkt: Packet) -> Option<Packet> {
@@ -31,9 +35,11 @@ impl Dispatcher {
if let Some(h) = self.table.get(&key) { if let Some(h) = self.table.get(&key) {
h(pkt).await h(pkt).await
} else { } else {
warn!(component = format!("0x{:04X}", key.0), warn!(
command = format!("0x{:04X}", key.1), component = format!("0x{:04X}", key.0),
"unhandled — returning empty response"); command = format!("0x{:04X}", key.1),
"unhandled — returning empty response"
);
Some(Packet::response_empty(&pkt)) Some(Packet::response_empty(&pkt))
} }
} }
+39 -25
View File
@@ -33,14 +33,19 @@ async fn main() -> anyhow::Result<()> {
) )
.init(); .init();
let cfg_path = std::env::args().nth(1).unwrap_or_else(|| "config.toml".into()); let cfg_path = std::env::args()
.nth(1)
.unwrap_or_else(|| "config.toml".into());
let cfg = config::Config::load(&cfg_path)?; let cfg = config::Config::load(&cfg_path)?;
info!(config = cfg_path, "loaded"); info!(config = cfg_path, "loaded");
let tls_cfg = tls::load_tls_config(&cfg.tls.cert, &cfg.tls.key)?; let tls_cfg = tls::load_tls_config(&cfg.tls.cert, &cfg.tls.key)?;
let acceptor = TlsAcceptor::from(Arc::clone(&tls_cfg)); let acceptor = TlsAcceptor::from(Arc::clone(&tls_cfg));
let capture = Arc::new(CaptureWriter::open(&cfg.capture.dir, cfg.capture.pretty_print)?); let capture = Arc::new(CaptureWriter::open(
&cfg.capture.dir,
cfg.capture.pretty_print,
)?);
let dispatcher = Arc::new(Dispatcher::new()); let dispatcher = Arc::new(Dispatcher::new());
let redir_addr: SocketAddr = cfg.redirector.listen.parse()?; let redir_addr: SocketAddr = cfg.redirector.listen.parse()?;
@@ -55,18 +60,22 @@ async fn main() -> anyhow::Result<()> {
// ── Redirector loop ─────────────────────────────────────────────────────── // ── Redirector loop ───────────────────────────────────────────────────────
let redir_acceptor = acceptor.clone(); let redir_acceptor = acceptor.clone();
let redir_cap = Arc::clone(&capture); let redir_cap = Arc::clone(&capture);
let adv_host = Arc::clone(&advertise_host); let adv_host = Arc::clone(&advertise_host);
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
match redir_listener.accept().await { match redir_listener.accept().await {
Err(e) => { error!(error=%e, "redirector accept error"); } Err(e) => {
error!(error=%e, "redirector accept error");
}
Ok((tcp, peer)) => { Ok((tcp, peer)) => {
let acc = redir_acceptor.clone(); let acc = redir_acceptor.clone();
let cap = Arc::clone(&redir_cap); let cap = Arc::clone(&redir_cap);
let host = Arc::clone(&adv_host); let host = Arc::clone(&adv_host);
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = handle_redirector(tcp, peer, acc, cap, &host, advertise_port).await { if let Err(e) =
handle_redirector(tcp, peer, acc, cap, &host, advertise_port).await
{
warn!(%peer, error=%e, "redirector session error"); warn!(%peer, error=%e, "redirector session error");
} }
}); });
@@ -78,10 +87,12 @@ async fn main() -> anyhow::Result<()> {
// ── Blaze loop ──────────────────────────────────────────────────────────── // ── Blaze loop ────────────────────────────────────────────────────────────
loop { loop {
match blaze_listener.accept().await { match blaze_listener.accept().await {
Err(e) => { error!(error=%e, "blaze accept error"); } Err(e) => {
error!(error=%e, "blaze accept error");
}
Ok((tcp, peer)) => { Ok((tcp, peer)) => {
let acc = acceptor.clone(); let acc = acceptor.clone();
let cap = Arc::clone(&capture); let cap = Arc::clone(&capture);
let disp = Arc::clone(&dispatcher); let disp = Arc::clone(&dispatcher);
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = handle_blaze(tcp, peer, acc, cap, disp).await { if let Err(e) = handle_blaze(tcp, peer, acc, cap, disp).await {
@@ -96,15 +107,15 @@ async fn main() -> anyhow::Result<()> {
// ── Redirector handler ──────────────────────────────────────────────────────── // ── Redirector handler ────────────────────────────────────────────────────────
async fn handle_redirector( async fn handle_redirector(
tcp : TcpStream, tcp: TcpStream,
peer : SocketAddr, peer: SocketAddr,
acceptor: TlsAcceptor, acceptor: TlsAcceptor,
capture : Arc<CaptureWriter>, capture: Arc<CaptureWriter>,
host : &str, host: &str,
port : u16, port: u16,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let tls = acceptor.accept(tcp).await?; let tls = acceptor.accept(tcp).await?;
let codec = PacketCodec::new(FramingVariant::Fire2); let codec = PacketCodec::new(FramingVariant::Fire2);
let mut io = Framed::new(tls, codec); let mut io = Framed::new(tls, codec);
let peer_str = peer.to_string(); let peer_str = peer.to_string();
@@ -120,7 +131,10 @@ async fn handle_redirector(
// capture will reveal it. For now we return our advertise address as a // capture will reveal it. For now we return our advertise address as a
// simple string blob so the client gets something to parse. // simple string blob so the client gets something to parse.
let body = build_redirector_body(host, port); let body = build_redirector_body(host, port);
let resp = Packet { frame: req.frame.response(), body }; let resp = Packet {
frame: req.frame.response(),
body,
};
capture.record(&resp, &peer_str, Dir::Out); capture.record(&resp, &peer_str, Dir::Out);
io.send(resp).await?; io.send(resp).await?;
} }
@@ -146,14 +160,14 @@ fn build_redirector_body(host: &str, port: u16) -> Bytes {
// ── Blaze session handler ───────────────────────────────────────────────────── // ── Blaze session handler ─────────────────────────────────────────────────────
async fn handle_blaze( async fn handle_blaze(
tcp : TcpStream, tcp: TcpStream,
peer : SocketAddr, peer: SocketAddr,
acceptor : TlsAcceptor, acceptor: TlsAcceptor,
capture : Arc<CaptureWriter>, capture: Arc<CaptureWriter>,
dispatcher: Arc<Dispatcher>, dispatcher: Arc<Dispatcher>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let tls = acceptor.accept(tcp).await?; let tls = acceptor.accept(tcp).await?;
let codec = PacketCodec::new(FramingVariant::Fire2); let codec = PacketCodec::new(FramingVariant::Fire2);
let mut io = Framed::new(tls, codec); let mut io = Framed::new(tls, codec);
let peer_str = peer.to_string(); let peer_str = peer.to_string();
+8 -7
View File
@@ -1,7 +1,10 @@
use std::{fs::File, io::BufReader, sync::Arc};
use rustls::{ServerConfig, pki_types::{CertificateDer, PrivateKeyDer}};
use rustls_pemfile::{certs, private_key};
use anyhow::Context; use anyhow::Context;
use rustls::{
pki_types::{CertificateDer, PrivateKeyDer},
ServerConfig,
};
use rustls_pemfile::{certs, private_key};
use std::{fs::File, io::BufReader, sync::Arc};
/// Load a TLS `ServerConfig` from PEM cert and key files. /// Load a TLS `ServerConfig` from PEM cert and key files.
/// ///
@@ -13,10 +16,8 @@ pub fn load_tls_config(cert_path: &str, key_path: &str) -> anyhow::Result<Arc<Se
// Install the ring crypto provider once. Harmless if called multiple times. // Install the ring crypto provider once. Harmless if called multiple times.
let _ = rustls::crypto::ring::default_provider().install_default(); let _ = rustls::crypto::ring::default_provider().install_default();
let cert_file = File::open(cert_path) let cert_file = File::open(cert_path).with_context(|| format!("open cert: {cert_path}"))?;
.with_context(|| format!("open cert: {cert_path}"))?; let key_file = File::open(key_path).with_context(|| format!("open key: {key_path}"))?;
let key_file = File::open(key_path)
.with_context(|| format!("open key: {key_path}"))?;
let certs: Vec<CertificateDer> = certs(&mut BufReader::new(cert_file)) let certs: Vec<CertificateDer> = certs(&mut BufReader::new(cert_file))
.collect::<Result<_, _>>() .collect::<Result<_, _>>()