1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
//! Advanced UDP Hole Punching Implementation
//!
//! This module implements Method 3 UDP hole punching as described in the paper
//! "NAT Traversal Techniques in Peer-to-Peer Applications". The technique uses
//! variable TTL values to optimize NAT traversal success rates, with a fallback
//! recovery mode for asymmetric failures.
//!
//! # Features
//!
//! - Progressive TTL packet transmission
//! - Asymmetric failure recovery
//! - Dual-stack IPv4/IPv6 support
//! - Encrypted configuration exchange
//! - Connection state tracking
//! - Mutual exclusion for UDP sockets
//!
//! # Examples
//!
//! ```rust,no_run
//! use citadel_wire::udp_traversal::linear::method3::Method3;
//! use citadel_wire::udp_traversal::linear::encrypted_config_container::HolePunchConfigContainer;
//! use citadel_wire::nat_identification::NatType;
//! use citadel_wire::error::FirewallError;
//! use citadel_io::tokio::net::UdpSocket;
//! use std::net::SocketAddr;
//! use netbeam::sync::RelativeNodeType;
//! use anyhow::Result;
//!
//! async fn example() -> Result<(), FirewallError> {
//!     // Create socket and config
//!     let socket = UdpSocket::bind("0.0.0.0:0").await?;
//!     let target_addr = "127.0.0.1:8080".parse::<SocketAddr>().unwrap();
//!     let config = HolePunchConfigContainer::default();
//!     
//!     // Create method3 instance
//!     let method = Method3::new(
//!         RelativeNodeType::Initiator,
//!         config,
//!         Default::default()
//!     );
//!     
//!     Ok(())
//! }
//! ```
//!
//! # Important Notes
//!
//! - Requires pre-process stage completion
//! - TTL values affect traversal success
//! - Recovery mode handles asymmetric failures
//! - Thread-safe socket operations
//! - Encrypted packet exchange
//!
//! # Related Components
//!
//! - [`crate::udp_traversal::linear::encrypted_config_container`] - Config encryption
//! - [`crate::socket_helpers`] - Socket utilities
//! - [`crate::nat_identification`] - NAT analysis
//! - [`crate::standard::upnp_handler`] - UPnP support
//!

use std::collections::{HashMap, HashSet};
use std::io::ErrorKind;
use std::net::SocketAddr;

use citadel_io::time::Duration;
use citadel_io::tokio::net::UdpSocket;
use citadel_io::tokio::sync::Mutex as TokioMutex;
use serde::{Deserialize, Serialize};

use crate::error::FirewallError;
use crate::socket_helpers::ensure_ipv6;
use crate::udp_traversal::hole_punched_socket::TargettedSocketAddr;
use crate::udp_traversal::linear::encrypted_config_container::HolePunchConfigContainer;
use crate::udp_traversal::HolePunchID;
use citadel_io::Mutex;
use netbeam::sync::RelativeNodeType;
use std::sync::Arc;

/// Method three: "Both sides send packets with short TTL values followed by packets with long TTL
// values". Source: page 7 of https://thomaspbraun.com/pdfs/NAT_Traversal/NAT_Traversal.pdf
pub struct Method3 {
    this_node_type: RelativeNodeType,
    encrypted_config: HolePunchConfigContainer,
    unique_id: HolePunchID,
    // in the case the adjacent node for id=key succeeds, yet, this node fails, recovery mode can ensue
    observed_addrs_on_syn: Mutex<HashMap<HolePunchID, TargettedSocketAddr>>,
}

#[derive(Serialize, Deserialize)]
enum NatPacket {
    Syn(HolePunchID, u32, RelativeNodeType, SocketAddr),
    // contains the local bind addr of candidate for socket identification
    SynAck(HolePunchID, RelativeNodeType, SocketAddr, HolePunchID),
    Check,
}

impl Method3 {
    /// Make sure to complete the pre-process stage before calling this
    pub fn new(
        this_node_type: RelativeNodeType,
        encrypted_config: HolePunchConfigContainer,
        unique_id: HolePunchID,
    ) -> Self {
        Self {
            this_node_type,
            encrypted_config,
            unique_id,
            observed_addrs_on_syn: Mutex::new(HashMap::new()),
        }
    }

    pub(crate) async fn execute(
        &self,
        socket: &UdpSocket,
        endpoints: &[SocketAddr],
    ) -> Result<TargettedSocketAddr, FirewallError> {
        match self.this_node_type {
            RelativeNodeType::Initiator => self.execute_either(socket, endpoints).await,
            RelativeNodeType::Receiver => self.execute_either(socket, endpoints).await,
        }
    }

    pub(crate) fn get_peer_external_addr_from_peer_hole_punch_id(
        &self,
        id: HolePunchID,
    ) -> Option<TargettedSocketAddr> {
        let lock = self.observed_addrs_on_syn.lock();
        log::trace!(target: "citadel", "Recv'd SYNS: {:?}", &*lock);
        lock.get(&id).copied()
    }

    /// The initiator must pass a vector correlating to the target endpoints. Each provided socket will attempt to reach out to the target endpoint (1-1)
    ///
    /// Note! The endpoints should be the port-predicted addrs
    #[cfg_attr(
        feature = "localhost-testing",
        tracing::instrument(level = "trace", target = "citadel", skip_all, ret, err(Debug))
    )]
    async fn execute_either(
        &self,
        socket: &UdpSocket,
        endpoints: &[SocketAddr],
    ) -> Result<TargettedSocketAddr, FirewallError> {
        let default_ttl = socket.ttl().ok();
        log::trace!(target: "citadel", "Default TTL: {default_ttl:?}");
        let unique_id = &self.unique_id.clone();
        let this_node_type = self.this_node_type;
        // We will begin sending packets right away, assuming the pre-process synchronization occurred
        // 400ms window
        let encryptor = &self.encrypted_config;
        let observed_addrs_on_syn = &self.observed_addrs_on_syn;

        let socket_wrapper = &UdpWrapper::new(socket);

        const MILLIS_DELTA: u64 = 20;
        let packet_send_params = &SendPacketBarrageParams {
            ttl_init: 20,
            delta_ttl: Some(60),
            socket: socket_wrapper,
            endpoints: &citadel_io::tokio::sync::Mutex::new(endpoints.iter().copied().collect()),
            encryptor,
            millis_delta: MILLIS_DELTA,
            count: 2,
            unique_id: *unique_id,
            this_node_type,
        };

        let receiver_task = async move {
            // we are only interested in the first receiver to receive a value
            let timeout = citadel_io::time::timeout(
                Duration::from_millis(3000),
                Self::recv_until(
                    socket_wrapper,
                    encryptor,
                    unique_id,
                    observed_addrs_on_syn,
                    MILLIS_DELTA,
                    this_node_type,
                    packet_send_params,
                ),
            );

            timeout.await.unwrap_or_else(|_| {
                Err(citadel_io::error!(
                    citadel_io::ErrorCode::FirewallPenetrationTimeout
                ))
            })
        };

        let sender_task = async move {
            //citadel_io::time::sleep(Duration::from_millis(10)).await; // wait to allow time for the joined receiver task to execute
            Self::send_packet_barrage(packet_send_params, None)
                .await
                .map_err(|err| FirewallError::firewall_hole_punch(err.to_string()))?;
            Ok(()) as Result<(), FirewallError>
        };

        let (res0, res1) = citadel_io::tokio::join!(receiver_task, sender_task);

        log::trace!(target: "citadel", "Hole-punch join result: recv={res0:?} and send={res1:?}");

        if let Some(default_ttl) = default_ttl {
            let _ = socket
                .set_ttl(default_ttl)
                .map_err(|err| FirewallError::firewall_hole_punch(err.to_string()));
        }

        let hole_punched_addr = res0?;

        log::trace!(target: "citadel", "Completed hole-punch...");

        Ok(hole_punched_addr)
    }

    /// Some research papers explain that incrementing the TTL on the packet may be beneficial
    #[allow(clippy::too_many_arguments)]
    async fn send_packet_barrage(
        params: &SendPacketBarrageParams<'_>,
        syn_received_addr: Option<(SocketAddr, HolePunchID)>,
    ) -> Result<(), anyhow::Error> {
        let SendPacketBarrageParams {
            ttl_init,
            delta_ttl,
            socket,
            endpoints,
            encryptor,
            millis_delta,
            count,
            unique_id,
            this_node_type,
        } = params;

        let mut sleep = citadel_io::time::interval(Duration::from_millis(*millis_delta));
        let delta_ttl = delta_ttl.unwrap_or(0);
        let ttls = (0..*count)
            .map(|idx| ttl_init + (idx * delta_ttl))
            .collect::<Vec<u32>>();

        // fan-out all packets from a singular source to multiple consumers using the ttls specified
        for ttl in ttls {
            let _ = sleep.tick().await;

            let mut endpoints_lock = endpoints.lock().await;

            for endpoint in endpoints_lock.clone() {
                let packet_ty = if let Some((syn_addr, peer_id_recv)) = syn_received_addr {
                    // put the addr the peer used to send to this node, that way the peer knows where
                    // to send the packet, even if the receive address is translated
                    NatPacket::SynAck(*unique_id, *this_node_type, syn_addr, peer_id_recv)
                } else {
                    // put the endpoint we are sending to in the payload, that way, once we get a SynAck, we know
                    // where our sent packet was sent that worked
                    NatPacket::Syn(*unique_id, ttl, *this_node_type, endpoint)
                    // SynAck
                };

                let packet_plaintext = bincode::serialize(&packet_ty).unwrap();

                let packet = encryptor.generate_packet(&packet_plaintext);
                log::trace!(target: "citadel", "Sending TTL={} to {} || {:?}", ttl, endpoint, &packet[..] as &[u8]);
                match socket.send(&packet, endpoint, Some(ttl)).await {
                    Ok(can_continue) => {
                        if !can_continue {
                            log::trace!(target: "citadel", "Early-terminating SYN barrage");
                            return Ok(());
                        }
                    }
                    Err(err) => {
                        let err_kind = err.kind();
                        if err_kind != ErrorKind::AddrNotAvailable {
                            log::warn!(target: "citadel", "Error sending packet from {:?} to {endpoint}: {:?}", socket.socket.local_addr()?, err);
                        }

                        if err.to_string().contains("NetworkUnreachable") {
                            endpoints_lock.remove(&endpoint);
                        }

                        if err_kind == ErrorKind::InvalidInput {
                            endpoints_lock.remove(&endpoint);
                        }
                    }
                }
            }

            if endpoints_lock.is_empty() {
                log::warn!(target: "citadel", "No endpoints to send to for {unique_id:?} (local bind: {})", socket.socket.local_addr()?);
                return Err(anyhow::Error::msg(
                    "All UDP endpoints are unreachable for NAT traversal",
                ));
            }
        }

        Ok(())
    }

    // Handles the reception of packets, as well as sending/awaiting for a verification
    async fn recv_until(
        socket: &UdpWrapper<'_>,
        encryptor: &HolePunchConfigContainer,
        unique_id: &HolePunchID,
        observed_addrs_on_syn: &Mutex<HashMap<HolePunchID, TargettedSocketAddr>>,
        _millis_delta: u64,
        this_node_type: RelativeNodeType,
        send_packet_params: &SendPacketBarrageParams<'_>,
    ) -> Result<TargettedSocketAddr, FirewallError> {
        let buf = &mut [0u8; 4096];
        log::trace!(target: "citadel", "[Hole-punch] Listening on {:?}", socket.socket.local_addr()?);

        let mut has_received_syn = false;
        loop {
            match socket.recv_from(buf).await {
                Ok((len, peer_external_addr)) => {
                    log::trace!(target: "citadel", "[UDP Hole-punch] RECV packet from {:?} | {:?}", &peer_external_addr, &buf[..len]);
                    let packet = match encryptor.decrypt_packet(&buf[..len]) {
                        Some(plaintext) => plaintext,
                        _ => {
                            log::warn!(target: "citadel", "BAD Hole-punch packet: decryption failed!");
                            continue;
                        }
                    };

                    match bincode::deserialize(&packet)
                        .map_err(|err| FirewallError::firewall_hole_punch(err.to_string()))
                    {
                        Ok(NatPacket::Check) => {
                            continue;
                        }
                        Ok(NatPacket::Syn(
                            peer_unique_id,
                            ttl,
                            adjacent_node_type,
                            their_send_addr,
                        )) => {
                            if has_received_syn {
                                continue;
                            }

                            if adjacent_node_type == this_node_type || &peer_unique_id == unique_id
                            {
                                log::warn!(target: "citadel", "RECV loopback packet; will discard");
                                continue;
                            }

                            log::trace!(target: "citadel", "RECV SYN from {peer_unique_id:?}");
                            let hole_punched_addr = TargettedSocketAddr::new(
                                peer_external_addr,
                                peer_external_addr,
                                peer_unique_id,
                            );

                            observed_addrs_on_syn
                                .lock()
                                .insert(peer_unique_id, hole_punched_addr);
                            log::trace!(target: "citadel", "Received TTL={ttl} packet from {peer_external_addr:?}. Awaiting mutual recognition... ");

                            has_received_syn = true;

                            let mut lock = send_packet_params.endpoints.lock().await;
                            let send_addrs = std::iter::once(peer_external_addr)
                                .chain(lock.iter().copied())
                                .collect::<HashSet<SocketAddr>>();
                            *lock = send_addrs;
                            drop(lock);

                            Self::send_packet_barrage(
                                send_packet_params,
                                Some((their_send_addr, peer_unique_id)),
                            )
                            .await
                            .map_err(|err| FirewallError::firewall_hole_punch(err.to_string()))?;
                        }

                        // the reception of a SynAck proves the existence of a hole punched since there is bidirectional communication through the NAT
                        Ok(NatPacket::SynAck(
                            adjacent_unique_id,
                            adjacent_node_type,
                            address_we_sent_to,
                            our_id,
                        )) => {
                            log::trace!(target: "citadel", "RECV SYN_ACK");
                            if adjacent_node_type == this_node_type {
                                log::warn!(target: "citadel", "RECV self-referential packet; will discard");
                                continue;
                            }

                            if &our_id != unique_id {
                                log::warn!(target: "citadel", "RECV Packet from wrong hole punching process. Received {our_id:?}, But expected our id of {unique_id:?}");
                                continue;
                            }

                            // NOTE: it is entirely possible that we receive a SynAck before even getting a Syn.
                            // Since we send SYNs to the other node, and, it's possible that we don't receive a SYN by the time
                            // the other node ACKs our sent out SYN, we should not terminate.

                            if peer_external_addr != address_we_sent_to {
                                let packet = bincode::serialize(&NatPacket::Check).unwrap();
                                // See if we can send a packet to the addr
                                if socket
                                    .socket
                                    .send_to(&packet, peer_external_addr)
                                    .await
                                    .is_ok()
                                {
                                    log::warn!(target: "citadel", "[will allow] RECV SYN_ACK that comes from the wrong addr. RECV: {peer_external_addr:?}, Expected: {address_we_sent_to:?}");
                                } else {
                                    log::warn!(target: "citadel", "[will NOT allow] RECV SYN_ACK that comes from the wrong addr. RECV: {peer_external_addr:?}, Expected: {address_we_sent_to:?}");
                                    continue;
                                }
                            }

                            // this means there was a successful ping-pong.
                            // initial should be address_we_went_to, natted would be peer_external_addr
                            let hole_punched_addr = TargettedSocketAddr::new(
                                address_we_sent_to,
                                peer_external_addr,
                                adjacent_unique_id,
                            );
                            log::trace!(target: "citadel", "***UDP Hole-punch to {:?} success!***", &hole_punched_addr);
                            socket.stop_outgoing_traffic().await;

                            return Ok(hole_punched_addr);
                        }

                        Err(err) => {
                            log::warn!(target: "citadel", "Unable to deserialize packet {:?} from {:?}: {:?}", &packet[..], peer_external_addr, err);
                        }
                    }
                }

                Err(err) => {
                    log::error!(
                        target: "citadel",
                        "Error receiving packet from {:?}: {err:?}",
                        socket.socket.local_addr()?
                    );

                    if err.kind() == ErrorKind::ConnectionReset {
                        return Err(citadel_io::error!(
                            citadel_io::ErrorCode::FirewallConnectionReset
                        ));
                    }
                }
            }
        }
    }
}

/// Used to enforce mutual exclusion writing
struct UdpWrapper<'a> {
    lock: Arc<TokioMutex<bool>>,
    socket: &'a UdpSocket,
}

impl UdpWrapper<'_> {
    fn new(socket: &UdpSocket) -> UdpWrapper<'_> {
        UdpWrapper {
            lock: Arc::new(TokioMutex::new(true)),
            socket,
        }
    }

    async fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, SocketAddr)> {
        self.socket.recv_from(buf).await
    }

    /// Returns false if
    async fn send(
        &self,
        buf: &[u8],
        mut to: SocketAddr,
        ttl: Option<u32>,
    ) -> std::io::Result<bool> {
        let lock = self.lock.lock().await;
        if !*lock {
            return Ok(false);
        }

        if let Some(ttl) = ttl {
            let _ = self.socket.set_ttl(ttl);
        }

        let local_addr = self.socket.local_addr()?;

        if to.is_ipv6() && local_addr.is_ipv4() {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "Send_to is ipv6, but bind is ipv4",
            ));
        }

        if local_addr.is_ipv6() {
            to = SocketAddr::V6(ensure_ipv6(to))
        }

        self.socket.send_to(buf, to).await?;
        Ok(true)
    }

    async fn stop_outgoing_traffic(&self) {
        *self.lock.lock().await = false
    }
}

struct SendPacketBarrageParams<'a> {
    ttl_init: u32,
    delta_ttl: Option<u32>,
    socket: &'a UdpWrapper<'a>,
    endpoints: &'a citadel_io::tokio::sync::Mutex<HashSet<SocketAddr>>,
    encryptor: &'a HolePunchConfigContainer,
    millis_delta: u64,
    count: u32,
    unique_id: HolePunchID,
    this_node_type: RelativeNodeType,
}