Skip to main content

trojan_analytics/
event.rs

1//! Connection event types.
2
3use std::net::IpAddr;
4
5use clickhouse::Row;
6use serde::{Deserialize, Serialize};
7use time::OffsetDateTime;
8
9/// A connection event representing the full lifecycle of a single connection.
10#[derive(Debug, Clone, Row, Serialize, Deserialize)]
11pub struct ConnectionEvent {
12    // === Time dimension ===
13    /// Connection start time (UTC).
14    #[serde(with = "clickhouse::serde::time::datetime64::millis")]
15    pub timestamp: OffsetDateTime,
16
17    /// Connection duration in milliseconds.
18    pub duration_ms: u64,
19
20    // === Connection identity ===
21    /// Connection ID (unique within server instance).
22    pub conn_id: u64,
23
24    /// Client IP address.
25    pub peer_ip: IpAddr,
26
27    /// Client port.
28    pub peer_port: u16,
29
30    // === User identity ===
31    /// User identifier (password hash prefix or custom ID).
32    pub user_id: String,
33
34    /// Authentication result.
35    pub auth_result: AuthResult,
36
37    // === Target information ===
38    /// Target address type.
39    pub target_type: TargetType,
40
41    /// Target host (IP or domain).
42    pub target_host: String,
43
44    /// Target port.
45    pub target_port: u16,
46
47    /// SNI (Server Name Indication), if available.
48    pub sni: String,
49
50    // === Traffic statistics ===
51    /// Bytes sent (client → server → target).
52    pub bytes_sent: u64,
53
54    /// Bytes received (target → server → client).
55    pub bytes_recv: u64,
56
57    /// Packets sent (UDP only).
58    pub packets_sent: u64,
59
60    /// Packets received (UDP only).
61    pub packets_recv: u64,
62
63    // === Connection metadata ===
64    /// Protocol type.
65    pub protocol: Protocol,
66
67    /// Transport layer.
68    pub transport: Transport,
69
70    /// Connection close reason.
71    pub close_reason: CloseReason,
72
73    /// Whether this was a fallback connection.
74    pub is_fallback: bool,
75
76    // === GeoIP information (peer_ip lookup) ===
77    /// Source country ISO 3166-1 alpha-2 code (e.g., "CN", "US").
78    pub peer_country: String,
79
80    /// Source region/state/province (e.g., "Shanghai", "California").
81    pub peer_region: String,
82
83    /// Source city (e.g., "Shanghai", "Los Angeles").
84    pub peer_city: String,
85
86    /// Source ASN number (e.g., 4134).
87    pub peer_asn: u32,
88
89    /// Source ASN organization (e.g., "China Telecom").
90    pub peer_org: String,
91
92    /// Source longitude.
93    pub peer_longitude: f64,
94
95    /// Source latitude.
96    pub peer_latitude: f64,
97
98    // === Server information ===
99    /// Server instance ID.
100    pub server_id: String,
101}
102
103impl ConnectionEvent {
104    /// Create a new connection event with default values.
105    pub fn new(conn_id: u64, peer_ip: IpAddr, peer_port: u16) -> Self {
106        Self {
107            timestamp: OffsetDateTime::now_utc(),
108            duration_ms: 0,
109            conn_id,
110            peer_ip,
111            peer_port,
112            user_id: String::new(),
113            auth_result: AuthResult::Skipped,
114            target_type: TargetType::Domain,
115            target_host: String::new(),
116            target_port: 0,
117            sni: String::new(),
118            bytes_sent: 0,
119            bytes_recv: 0,
120            packets_sent: 0,
121            packets_recv: 0,
122            protocol: Protocol::Tcp,
123            transport: Transport::Direct,
124            close_reason: CloseReason::Normal,
125            is_fallback: false,
126            peer_country: String::new(),
127            peer_region: String::new(),
128            peer_city: String::new(),
129            peer_asn: 0,
130            peer_org: String::new(),
131            peer_longitude: 0.0,
132            peer_latitude: 0.0,
133            server_id: String::new(),
134        }
135    }
136}
137
138/// Authentication result.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case")]
141pub enum AuthResult {
142    /// Authentication succeeded.
143    Success,
144    /// Authentication failed.
145    Failed,
146    /// Authentication was skipped (fallback traffic).
147    Skipped,
148}
149
150impl From<AuthResult> for &'static str {
151    fn from(r: AuthResult) -> Self {
152        match r {
153            AuthResult::Success => "success",
154            AuthResult::Failed => "failed",
155            AuthResult::Skipped => "skipped",
156        }
157    }
158}
159
160/// Target address type.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub enum TargetType {
164    /// IPv4 address.
165    Ipv4,
166    /// IPv6 address.
167    Ipv6,
168    /// Domain name.
169    Domain,
170}
171
172impl From<TargetType> for &'static str {
173    fn from(t: TargetType) -> Self {
174        match t {
175            TargetType::Ipv4 => "ipv4",
176            TargetType::Ipv6 => "ipv6",
177            TargetType::Domain => "domain",
178        }
179    }
180}
181
182/// Connection protocol.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case")]
185pub enum Protocol {
186    /// TCP connection.
187    Tcp,
188    /// UDP association.
189    Udp,
190}
191
192impl From<Protocol> for &'static str {
193    fn from(p: Protocol) -> Self {
194        match p {
195            Protocol::Tcp => "tcp",
196            Protocol::Udp => "udp",
197        }
198    }
199}
200
201/// Transport layer type.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204pub enum Transport {
205    /// Direct TLS connection.
206    Direct,
207    /// WebSocket transport.
208    WebSocket,
209}
210
211impl From<Transport> for &'static str {
212    fn from(t: Transport) -> Self {
213        match t {
214            Transport::Direct => "direct",
215            Transport::WebSocket => "websocket",
216        }
217    }
218}
219
220/// Connection close reason.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(rename_all = "snake_case")]
223pub enum CloseReason {
224    /// Normal close.
225    Normal,
226    /// Idle timeout.
227    Timeout,
228    /// Error occurred.
229    Error,
230    /// Connection reset.
231    Reset,
232    /// Server shutdown.
233    ServerShutdown,
234}
235
236impl From<CloseReason> for &'static str {
237    fn from(r: CloseReason) -> Self {
238        match r {
239            CloseReason::Normal => "normal",
240            CloseReason::Timeout => "timeout",
241            CloseReason::Error => "error",
242            CloseReason::Reset => "reset",
243            CloseReason::ServerShutdown => "shutdown",
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use std::net::{IpAddr, Ipv4Addr};
252
253    #[test]
254    fn connection_event_new_defaults() {
255        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
256        let event = ConnectionEvent::new(42, ip, 8080);
257
258        assert_eq!(event.conn_id, 42);
259        assert_eq!(event.peer_ip, ip);
260        assert_eq!(event.peer_port, 8080);
261        assert_eq!(event.duration_ms, 0);
262        assert!(event.user_id.is_empty());
263        assert_eq!(event.auth_result, AuthResult::Skipped);
264        assert_eq!(event.target_type, TargetType::Domain);
265        assert!(event.target_host.is_empty());
266        assert_eq!(event.target_port, 0);
267        assert_eq!(event.bytes_sent, 0);
268        assert_eq!(event.bytes_recv, 0);
269        assert_eq!(event.protocol, Protocol::Tcp);
270        assert_eq!(event.transport, Transport::Direct);
271        assert_eq!(event.close_reason, CloseReason::Normal);
272        assert!(!event.is_fallback);
273        // GeoIP fields default to empty
274        assert!(event.peer_country.is_empty());
275        assert!(event.peer_region.is_empty());
276        assert!(event.peer_city.is_empty());
277        assert_eq!(event.peer_asn, 0);
278        assert!(event.peer_org.is_empty());
279        assert_eq!(event.peer_longitude, 0.0);
280        assert_eq!(event.peer_latitude, 0.0);
281        assert!(event.server_id.is_empty());
282    }
283
284    #[test]
285    fn enum_into_str() {
286        let s: &str = AuthResult::Success.into();
287        assert_eq!(s, "success");
288        let s: &str = AuthResult::Failed.into();
289        assert_eq!(s, "failed");
290        let s: &str = AuthResult::Skipped.into();
291        assert_eq!(s, "skipped");
292
293        let s: &str = TargetType::Ipv4.into();
294        assert_eq!(s, "ipv4");
295        let s: &str = TargetType::Ipv6.into();
296        assert_eq!(s, "ipv6");
297        let s: &str = TargetType::Domain.into();
298        assert_eq!(s, "domain");
299
300        let s: &str = Protocol::Tcp.into();
301        assert_eq!(s, "tcp");
302        let s: &str = Protocol::Udp.into();
303        assert_eq!(s, "udp");
304
305        let s: &str = Transport::Direct.into();
306        assert_eq!(s, "direct");
307        let s: &str = Transport::WebSocket.into();
308        assert_eq!(s, "websocket");
309
310        let s: &str = CloseReason::Normal.into();
311        assert_eq!(s, "normal");
312        let s: &str = CloseReason::Timeout.into();
313        assert_eq!(s, "timeout");
314        let s: &str = CloseReason::Error.into();
315        assert_eq!(s, "error");
316        let s: &str = CloseReason::Reset.into();
317        assert_eq!(s, "reset");
318        let s: &str = CloseReason::ServerShutdown.into();
319        assert_eq!(s, "shutdown");
320    }
321
322    #[test]
323    fn enum_serde_roundtrip() {
324        let auth = AuthResult::Failed;
325        let json = serde_json::to_string(&auth).unwrap();
326        assert_eq!(json, "\"failed\"");
327        let back: AuthResult = serde_json::from_str(&json).unwrap();
328        assert_eq!(back, AuthResult::Failed);
329
330        let proto = Protocol::Udp;
331        let json = serde_json::to_string(&proto).unwrap();
332        assert_eq!(json, "\"udp\"");
333        let back: Protocol = serde_json::from_str(&json).unwrap();
334        assert_eq!(back, Protocol::Udp);
335
336        let transport = Transport::WebSocket;
337        let json = serde_json::to_string(&transport).unwrap();
338        assert_eq!(json, "\"web_socket\"");
339        let back: Transport = serde_json::from_str(&json).unwrap();
340        assert_eq!(back, Transport::WebSocket);
341    }
342}