use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::timestamp::Timestamp;
wire_enum! {
pub enum ProxyType {
FreeVpn = "FREE_VPN",
CommercialVpn = "COMMERCIAL_VPN",
EnterpriseVpn = "ENTERPRISE_VPN",
MobileProxy = "MOBILE_PROXY",
BlockchainProxy = "BLOCKCHAIN_PROXY",
ResidentialProxy = "RESIDENTIAL_PROXY",
PublicProxy = "PUBLIC_PROXY",
DatacenterProxy = "DATACENTER_PROXY",
TorNode = "TOR_NODE",
PrivateRelay = "PRIVATE_RELAY",
Botnet = "BOTNET",
SearchEngine = "SEARCH_ENGINE",
AiCrawler = "AI_CRAWLER",
SocialMedia = "SOCIAL_MEDIA",
UptimeMonitor = "UPTIME_MONITOR",
LinkPreview = "LINK_PREVIEW",
SeoCrawler = "SEO_CRAWLER",
WebArchiver = "WEB_ARCHIVER",
WebhookProvider = "WEBHOOK_PROVIDER",
PaymentProcessor = "PAYMENT_PROCESSOR",
InternetScanner = "INTERNET_SCANNER",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ProxyEvent {
pub ip: Option<String>,
pub provider: Option<String>,
pub r#type: Option<ProxyType>,
pub country_code: Option<String>,
pub asn: Option<u32>,
pub timestamp: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AnonymizerEvent {
pub range_start: Option<String>,
pub range_end: Option<String>,
pub provider: Option<String>,
pub r#type: Option<ProxyType>,
pub country_code: Option<String>,
pub asn: Option<u32>,
pub timestamp: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TorrentEvent {
pub info_hash: Option<String>,
pub name: Option<String>,
pub magnet_uri: Option<String>,
pub total_size: Option<i64>,
pub piece_length: Option<i64>,
pub file_count: Option<i64>,
pub files: Option<Vec<TorrentFile>>,
pub peers: Option<Vec<TorrentPeer>>,
pub timestamp: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TorrentFile {
pub path: Option<String>,
pub length: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TorrentPeer {
pub ip: Option<String>,
pub port: Option<u16>,
pub source: Option<String>,
pub encrypted: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Ja4tEvent {
pub ip: Option<String>,
pub ja4t: Option<String>,
pub provider: Option<String>,
pub r#type: Option<ProxyType>,
pub country_code: Option<String>,
pub asn: Option<u32>,
pub timestamp: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosMeta {
pub proxy_ip: Option<String>,
pub server: Option<String>,
pub pool_id: Option<String>,
pub provider: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosHttpEvent {
pub timestamp: Timestamp,
pub tunnel_id: u64,
pub domain: String,
pub port: u16,
pub protocol: String,
pub meta: HeliosMeta,
pub details: Option<HttpRequestDetails>,
#[serde(default, with = "base64_bytes")]
pub raw: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HttpRequestDetails {
pub method: Option<String>,
pub uri: Option<String>,
pub version: Option<String>,
pub headers: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosHttpsEvent {
pub timestamp: Timestamp,
pub tunnel_id: u64,
pub domain: String,
pub port: u16,
pub protocol: String,
pub meta: HeliosMeta,
pub details: Option<TlsClientHello>,
pub raw: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TlsClientHello {
pub record_version: Option<String>,
pub handshake_version: Option<String>,
pub client_random: Option<String>,
pub session_id: Option<String>,
pub session_id_length: Option<u8>,
pub cipher_suites: Option<Vec<TlsCodeName>>,
pub compression_methods: Option<Vec<u8>>,
pub extensions: Option<Vec<TlsExtension>>,
pub sni: Option<String>,
pub alpn: Option<Vec<String>>,
pub supported_versions: Option<Vec<String>>,
pub supported_groups: Option<Vec<TlsCodeName>>,
pub signature_algorithms: Option<Vec<TlsCodeName>>,
pub ec_point_formats: Option<Vec<String>>,
pub key_share_groups: Option<Vec<String>>,
pub psk_key_exchange_modes: Option<Vec<String>>,
pub compress_certificate: Option<Vec<String>>,
pub has_grease: Option<bool>,
pub application_settings: Option<bool>,
pub delegated_credentials: Option<bool>,
pub encrypt_then_mac: Option<bool>,
pub extended_master_secret: Option<bool>,
pub post_handshake_auth: Option<bool>,
pub renegotiation_info: Option<bool>,
pub signed_certificate_timestamps: Option<bool>,
pub status_request: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TlsCodeName {
pub code: Option<u16>,
pub name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TlsExtension {
pub code: Option<u16>,
pub name: Option<String>,
pub length: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosDnsEvent {
pub timestamp: Timestamp,
pub tunnel_id: u64,
pub domain: String,
pub port: u16,
pub protocol: String,
pub meta: HeliosMeta,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosAdbEvent {
pub session: String,
pub sequential_id: u64,
pub command: String,
pub hash: String,
}
mod base64_bytes {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use serde::{Deserialize, Deserializer, Serializer};
pub(super) fn serialize<S: Serializer>(
bytes: &Option<Vec<u8>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match bytes {
Some(bytes) => serializer.serialize_some(&STANDARD.encode(bytes)),
None => serializer.serialize_none(),
}
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Vec<u8>>, D::Error> {
let Some(text) = Option::<String>::deserialize(deserializer)? else {
return Ok(None);
};
STANDARD
.decode(text)
.map(Some)
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_tunnel_ids_above_i64_max() {
let line = r#"{"timestamp":1778200137487,"tunnel_id":17257744507466107949,"domain":"example.com","port":443,"protocol":"dns","meta":{}}"#;
let event: HeliosDnsEvent = serde_json::from_str(line).unwrap();
assert_eq!(event.tunnel_id, 17_257_744_507_466_107_949);
assert!(
serde_json::to_string(&event)
.unwrap()
.contains("17257744507466107949")
);
}
#[test]
fn decodes_base64_raw_requests() {
let line = r#"{"timestamp":1,"tunnel_id":1,"domain":"d","port":80,"protocol":"http","meta":{},"raw":"R0VUIC8gSFRUUC8xLjENCg=="}"#;
let event: HeliosHttpEvent = serde_json::from_str(line).unwrap();
assert_eq!(event.raw.as_deref(), Some(&b"GET / HTTP/1.1\r\n"[..]));
let missing =
r#"{"timestamp":1,"tunnel_id":1,"domain":"d","port":80,"protocol":"http","meta":{}}"#;
let event: HeliosHttpEvent = serde_json::from_str(missing).unwrap();
assert_eq!(event.raw, None);
let invalid = r#"{"timestamp":1,"tunnel_id":1,"domain":"d","port":80,"protocol":"http","meta":{},"raw":"not base64!"}"#;
assert!(serde_json::from_str::<HeliosHttpEvent>(invalid).is_err());
}
#[test]
fn unknown_enum_values_are_kept() {
let event: ProxyEvent = serde_json::from_str(r#"{"type":"NEW_KIND"}"#).unwrap();
assert_eq!(event.r#type, Some(ProxyType::Unknown("NEW_KIND".into())));
assert_eq!(
serde_json::to_string(&event.r#type).unwrap(),
r#""NEW_KIND""#
);
}
#[test]
fn enums_display_with_padding() {
assert_eq!(format!("[{:<10}]", ProxyType::FreeVpn), "[FREE_VPN ]");
assert_eq!(
format!("[{:>5}]", ProxyType::Unknown("NEW".into())),
"[ NEW]"
);
}
#[test]
fn wrong_types_are_rejected() {
assert!(serde_json::from_str::<ProxyEvent>(r#"{"asn":"13335"}"#).is_err());
assert!(serde_json::from_str::<ProxyEvent>(r#"{"asn":4294967296}"#).is_err());
assert!(serde_json::from_str::<ProxyEvent>(r#"{"type":7}"#).is_err());
}
}