18 Commits

Author SHA1 Message Date
Datong Sun
8a74b31c6e chore(phantun) bump fake-tcp dependency to v0.3.0 and release
`v0.3.1`
2022-04-10 01:37:59 -07:00
Datong Sun
ca14ba457f chore(fake-tcp) bump to v0.3.0 2022-04-10 01:36:45 -07:00
Datong Sun
33a0cfe567 docs(README) updated benchmarking results 2022-04-10 01:35:07 -07:00
Datong Sun
95dfd8ab54 fix(fake-tcp) fix an issue where RST generated is not following
the proper RFC requirement.

Send ACK every 128MB in lieu of data packets.
2022-04-10 16:33:53 +08:00
Datong Sun
1c35635091 docs(README) bump latest release version to v0.3.0 2022-04-09 08:49:29 -07:00
Datong Sun
b8a6c8853b chore(phantun) bump to v0.3.0 2022-04-09 08:39:44 -07:00
Datong Sun
d97a27778b style(phantun) refactor out common functions and constants 2022-04-09 21:32:07 +08:00
Datong Sun
35f7b35ff5 perf(phantun) spawn multiple threads for UDP send/receive 2022-04-09 21:32:07 +08:00
Datong Sun
dff0c4ca28 docs(readme) add link for fake-tcp docs 2022-04-09 12:17:11 +08:00
Datong Sun
9bf78adc92 chore(fake-tcp) bump to v0.2.4 with new documentations 2022-04-08 21:10:36 -07:00
Datong Sun
5d4e3bf8c0 docs(fake-tcp) added documentations for fake-tcp 2022-04-09 12:10:13 +08:00
Datong Sun
9c85b43e94 style(phantun) use the clap::Command struct, removed the deprecated clap::App usage 2022-04-09 11:00:20 +08:00
Datong Sun
66b0bc11b0 chore(phantun) use path dependency for fake-tcp crate 2022-04-09 11:00:20 +08:00
Datong Sun
02b00dfc3a docs(images) updated the flow diagram 2022-03-22 05:16:31 -07:00
dependabot[bot]
0ee7774d03 chore(deps): bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-03 10:40:48 +08:00
dependabot[bot]
11fdac78f1 chore(deps): update pnet requirement from 0.28 to 0.29
Updates the requirements on [pnet](https://github.com/libpnet/libpnet) to permit the latest version.
- [Release notes](https://github.com/libpnet/libpnet/releases)
- [Commits](https://github.com/libpnet/libpnet/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: pnet
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-14 23:44:23 +08:00
Datong Sun
ed686ce9fa docs(licenses) updated to year 2022 2022-01-03 07:57:09 -08:00
Datong Sun
d9001b08aa docs(readme) bumped latest release to v0.2.5 2022-01-03 07:54:41 -08:00
17 changed files with 325 additions and 135 deletions

View File

@@ -31,7 +31,7 @@ jobs:
- mipsel-unknown-linux-musl - mipsel-unknown-linux-musl
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable

View File

@@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2014-2021 The Rust Project Developers Copyright 2021-2022 Datong Sun (dndx@idndx.com)
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2014-2021 The Rust Project Developers Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

View File

@@ -24,6 +24,7 @@ Table of Contents
* [MTU overhead](#mtu-overhead) * [MTU overhead](#mtu-overhead)
* [MTU calculation for WireGuard](#mtu-calculation-for-wireguard) * [MTU calculation for WireGuard](#mtu-calculation-for-wireguard)
* [Version compatibility](#version-compatibility) * [Version compatibility](#version-compatibility)
* [Documentations](#documentations)
* [Performance](#performance) * [Performance](#performance)
* [Future plans](#future-plans) * [Future plans](#future-plans)
* [Compariation to udp2raw](#compariation-to-udp2raw) * [Compariation to udp2raw](#compariation-to-udp2raw)
@@ -31,7 +32,7 @@ Table of Contents
# Latest release # Latest release
[v0.2.4](https://github.com/dndx/phantun/releases/tag/v0.2.4) [v0.3.0](https://github.com/dndx/phantun/releases/tag/v0.3.0)
# Overview # Overview
@@ -71,7 +72,7 @@ NIC address and Phantun's TUN interface address.
You may customize the name of Tun interface created by Phantun and the assigned addresses. Please You may customize the name of Tun interface created by Phantun and the assigned addresses. Please
run the executable with `-h` options to see how to change them. run the executable with `-h` options to see how to change them.
Another way to help understand this network topology: Another way to help understand this network topology (please see the diagram above for an illustration of this topology):
Phantun Client is like a machine with private IP address (`192.168.200.2`) behind a router. Phantun Client is like a machine with private IP address (`192.168.200.2`) behind a router.
In order for it to reach the Internet, you will need to SNAT the private IP address before it's traffic In order for it to reach the Internet, you will need to SNAT the private IP address before it's traffic
@@ -176,6 +177,8 @@ sudo setcap cap_net_admin=+pe phantun_client
**Note:** Run Phantun executable with `-h` option to see full detailed options. **Note:** Run Phantun executable with `-h` option to see full detailed options.
[Back to TOC](#table-of-contents)
### Server ### Server
Note: `4567` is the TCP port Phantun should listen on and must corresponds to the DNAT Note: `4567` is the TCP port Phantun should listen on and must corresponds to the DNAT
@@ -252,15 +255,25 @@ of Server/Client of Phantun on both ends to ensure maximum compatibility.
[Back to TOC](#table-of-contents) [Back to TOC](#table-of-contents)
# Documentations
For users who wish to use `fake-tcp` library inside their own project, refer to the documentations for the library at:
[https://docs.rs/fake-tcp](https://docs.rs/fake-tcp).
[Back to TOC](#table-of-contents)
# Performance # Performance
Performance was tested on AWS t3.xlarge instance with 4 vCPUs and 5 Gb/s NIC. WireGuard was used Performance was tested on 2 AWS `t4g.xlarge` instances with 4 vCPUs and 5 Gb/s NIC over LAN. `nftables` was used to redirect
for tunneling TCP/UDP traffic between two test instances and MTU has been tuned to avoid fragmentation. UDP stream of `iperf3` to go through the Phantun/udp2raw tunnel between two test instances and MTU has been tuned to avoid fragmentation.
| | WireGuard | WireGuard + Phantun | WireGuard + udp2raw (cipher-mode=none auth-mode=none disable-anti-replay) | Test command: `iperf3 -c <IP> -p <PORT> -R -u -l 1400 -b 1000m -t 30 -P 5`
|-----------------|-------------|---------------------|---------------------------------------------------------------------------|
| iperf3 -c IP -R | 1.56 Gbit/s | 540 Mbit/s | 369 Mbit/s | | Mode | Speed | Overall CPU Usage |
| iperf3 -c IP | 1.71 Gbit/s | 519 Mbit/s | 312 Mbit/s | |---------------------------------------------------------------|----------------|--------------------------|
| Direct connection | 3.35 Gbits/sec | 25% (1 core at 100%) |
| Phantun | 2.03 Gbits/sec | 95% (all cores utilized) |
| udp2raw (cipher-mode=none auth-mode=none disable-anti-replay) | 876 Mbits/sec | 50% (2 cores at 100%) |
[Back to TOC](#table-of-contents) [Back to TOC](#table-of-contents)
@@ -300,7 +313,7 @@ Here is a quick overview of comparison between those two to help you choose:
# License # License
Copyright 2021 Datong Sun <dndx@idndx.com> Copyright 2021-2022 Datong Sun (dndx@idndx.com)
Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)> or the MIT license [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)> or the MIT license

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "fake-tcp" name = "fake-tcp"
version = "0.2.3" version = "0.3.0"
edition = "2021" edition = "2021"
authors = ["Datong Sun <dndx@idndx.com>"] authors = ["Datong Sun <dndx@idndx.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -16,7 +16,7 @@ benchmark = []
[dependencies] [dependencies]
bytes = "1" bytes = "1"
pnet = "0.28" pnet = "0.29"
tokio = { version = "1.14", features = ["full"] } tokio = { version = "1.14", features = ["full"] }
rand = { version = "0.8", features = ["small_rng"] } rand = { version = "0.8", features = ["small_rng"] }
log = "0.4" log = "0.4"

View File

@@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2014-2021 The Rust Project Developers Copyright 2021-2022 Datong Sun (dndx@idndx.com)
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2014-2021 The Rust Project Developers Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

View File

@@ -1,3 +1,43 @@
//! A minimum, userspace TCP based datagram stack
//!
//! # Overview
//!
//! `fake-tcp` is a reusable library that implements a minimum TCP stack in
//! user space using the Tun interface. It allows programs to send datagrams
//! as if they are part of a TCP connection. `fake-tcp` has been tested to
//! be able to pass through a variety of NAT and stateful firewalls while
//! fully preserves certain desirable behavior such as out of order delivery
//! and no congestion/flow controls.
//!
//! # Core Concepts
//!
//! The core of the `fake-tcp` crate compose of two structures. [`Stack`] and
//! [`Socket`].
//!
//! ## [`Stack`]
//!
//! [`Stack`] represents a virtual TCP stack that operates at
//! Layer 3. It is responsible for:
//!
//! * TCP active and passive open and handshake
//! * `RST` handling
//! * Interact with the Tun interface at Layer 3
//! * Distribute incoming datagrams to corresponding [`Socket`]
//!
//! ## [`Socket`]
//!
//! [`Socket`] represents a TCP connection. It registers the identifying
//! tuple `(src_ip, src_port, dest_ip, dest_port)` inside the [`Stack`] so
//! so that incoming packets can be distributed to the right [`Socket`] with
//! using a channel. It is also what the client should use for
//! sending/receiving datagrams.
//!
//! # Examples
//!
//! Please see [`client.rs`](https://github.com/dndx/phantun/blob/main/phantun/src/bin/client.rs)
//! and [`server.rs`](https://github.com/dndx/phantun/blob/main/phantun/src/bin/server.rs) files
//! from the `phantun` crate for how to use this library in client/server mode, respectively.
#![cfg_attr(feature = "benchmark", feature(test))] #![cfg_attr(feature = "benchmark", feature(test))]
pub mod packet; pub mod packet;
@@ -21,9 +61,10 @@ use tokio_tun::Tun;
const TIMEOUT: time::Duration = time::Duration::from_secs(1); const TIMEOUT: time::Duration = time::Duration::from_secs(1);
const RETRIES: usize = 6; const RETRIES: usize = 6;
const MPSC_BUFFER_LEN: usize = 512; const MPSC_BUFFER_LEN: usize = 512;
const MAX_UNACKED_LEN: u32 = 128 * 1024 * 1024; // 128MB
#[derive(Hash, Eq, PartialEq, Clone, Debug)] #[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub struct AddrTuple { struct AddrTuple {
local_addr: SocketAddrV4, local_addr: SocketAddrV4,
remote_addr: SocketAddrV4, remote_addr: SocketAddrV4,
} }
@@ -66,9 +107,17 @@ pub struct Socket {
remote_addr: SocketAddrV4, remote_addr: SocketAddrV4,
seq: AtomicU32, seq: AtomicU32,
ack: AtomicU32, ack: AtomicU32,
last_ack: AtomicU32,
state: State, state: State,
} }
/// A socket that represents a unique TCP connection between a server and client.
///
/// The `Socket` object itself satisfies `Sync` and `Send`, which means it can
/// be safely called within an async future.
///
/// To close a TCP connection that is no longer needed, simply drop this object
/// out of scope.
impl Socket { impl Socket {
fn new( fn new(
shared: Arc<Shared>, shared: Arc<Shared>,
@@ -89,6 +138,7 @@ impl Socket {
remote_addr, remote_addr,
seq: AtomicU32::new(0), seq: AtomicU32::new(0),
ack: AtomicU32::new(ack.unwrap_or(0)), ack: AtomicU32::new(ack.unwrap_or(0)),
last_ack: AtomicU32::new(ack.unwrap_or(0)),
state, state,
}, },
incoming_tx, incoming_tx,
@@ -96,32 +146,44 @@ impl Socket {
} }
fn build_tcp_packet(&self, flags: u16, payload: Option<&[u8]>) -> Bytes { fn build_tcp_packet(&self, flags: u16, payload: Option<&[u8]>) -> Bytes {
let ack = self.ack.load(Ordering::Relaxed);
self.last_ack.store(ack, Ordering::Relaxed);
build_tcp_packet( build_tcp_packet(
self.local_addr, self.local_addr,
self.remote_addr, self.remote_addr,
self.seq.load(Ordering::Relaxed), self.seq.load(Ordering::Relaxed),
self.ack.load(Ordering::Relaxed), ack,
flags, flags,
payload, payload,
) )
} }
/// Sends a datagram to the other end.
///
/// This method takes `&self`, and it can be called safely by multiple threads
/// at the same time.
///
/// A return of `None` means the Tun socket returned an error
/// and this socket must be closed.
pub async fn send(&self, payload: &[u8]) -> Option<()> { pub async fn send(&self, payload: &[u8]) -> Option<()> {
match self.state { match self.state {
State::Established => { State::Established => {
let buf = self.build_tcp_packet(tcp::TcpFlags::ACK, Some(payload)); let buf = self.build_tcp_packet(tcp::TcpFlags::ACK, Some(payload));
self.seq.fetch_add(payload.len() as u32, Ordering::Relaxed); self.seq.fetch_add(payload.len() as u32, Ordering::Relaxed);
self.tun.send(&buf).await.ok().and(Some(()))
tokio::select! {
res = self.tun.send(&buf) => {
res.ok().and(Some(()))
},
}
} }
_ => unreachable!(), _ => unreachable!(),
} }
} }
/// Attempt to receive a datagram from the other end.
///
/// This method takes `&self`, and it can be called safely by multiple threads
/// at the same time.
///
/// A return of `None` means the TCP connection is broken
/// and this socket must be closed.
pub async fn recv(&self, buf: &mut [u8]) -> Option<usize> { pub async fn recv(&self, buf: &mut [u8]) -> Option<usize> {
match self.state { match self.state {
State::Established => { State::Established => {
@@ -136,8 +198,18 @@ impl Socket {
let payload = tcp_packet.payload(); let payload = tcp_packet.payload();
self.ack let new_ack = tcp_packet.get_sequence().wrapping_add(payload.len() as u32);
.store(tcp_packet.get_sequence().wrapping_add(1), Ordering::Relaxed); let last_ask = self.last_ack.load(Ordering::Relaxed);
self.ack.store(new_ack, Ordering::Relaxed);
if new_ack.overflowing_sub(last_ask).0 > MAX_UNACKED_LEN {
let buf = self.build_tcp_packet(tcp::TcpFlags::ACK, None);
if let Err(e) = self.tun.try_send(&buf) {
// This should not really happen as we have not sent anything for
// quite some time...
info!("Connection {} unable to send idling ACK back: {}", self, e)
}
}
buf[..payload.len()].copy_from_slice(payload); buf[..payload.len()].copy_from_slice(payload);
@@ -246,6 +318,7 @@ impl Socket {
} }
impl Drop for Socket { impl Drop for Socket {
/// Drop the socket and close the TCP connection
fn drop(&mut self) { fn drop(&mut self) {
let tuple = AddrTuple::new(self.local_addr, self.remote_addr); let tuple = AddrTuple::new(self.local_addr, self.remote_addr);
// dissociates ourself from the dispatch map // dissociates ourself from the dispatch map
@@ -253,7 +326,14 @@ impl Drop for Socket {
// purge cache // purge cache
self.shared.tuples_purge.send(tuple).unwrap(); self.shared.tuples_purge.send(tuple).unwrap();
let buf = self.build_tcp_packet(tcp::TcpFlags::RST, None); let buf = build_tcp_packet(
self.local_addr,
self.remote_addr,
self.seq.load(Ordering::Relaxed),
0,
tcp::TcpFlags::RST,
None,
);
if let Err(e) = self.tun.try_send(&buf) { if let Err(e) = self.tun.try_send(&buf) {
warn!("Unable to send RST to remote end: {}", e); warn!("Unable to send RST to remote end: {}", e);
} }
@@ -263,6 +343,7 @@ impl Drop for Socket {
} }
impl fmt::Display for Socket { impl fmt::Display for Socket {
/// User-friendly string representation of the socket
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
@@ -272,7 +353,12 @@ impl fmt::Display for Socket {
} }
} }
/// A userspace TCP state machine
impl Stack { impl Stack {
/// Create a new stack, `tun` is an array of [`Tun`](tokio_tun::Tun).
/// When more than one [`Tun`](tokio_tun::Tun) object is passed in, same amount
/// of reader will be spawned later. This allows user to utilize the performance
/// benefit of Multiqueue Tun support on machines with SMP.
pub fn new(tun: Vec<Tun>) -> Stack { pub fn new(tun: Vec<Tun>) -> Stack {
let tun: Vec<Arc<Tun>> = tun.into_iter().map(Arc::new).collect(); let tun: Vec<Arc<Tun>> = tun.into_iter().map(Arc::new).collect();
let (ready_tx, ready_rx) = mpsc::channel(MPSC_BUFFER_LEN); let (ready_tx, ready_rx) = mpsc::channel(MPSC_BUFFER_LEN);
@@ -301,14 +387,18 @@ impl Stack {
} }
} }
/// Listens for incoming connections on the given `port`.
pub fn listen(&mut self, port: u16) { pub fn listen(&mut self, port: u16) {
assert!(self.shared.listening.write().unwrap().insert(port)); assert!(self.shared.listening.write().unwrap().insert(port));
} }
/// Accepts an incoming connection.
pub async fn accept(&mut self) -> Socket { pub async fn accept(&mut self) -> Socket {
self.ready.recv().await.unwrap() self.ready.recv().await.unwrap()
} }
/// Connects to the remote end. `None` returned means
/// the connection attempt failed.
pub async fn connect(&mut self, addr: SocketAddrV4) -> Option<Socket> { pub async fn connect(&mut self, addr: SocketAddrV4) -> Option<Socket> {
let mut rng = SmallRng::from_entropy(); let mut rng = SmallRng::from_entropy();
let local_port: u16 = rng.gen_range(1024..65535); let local_port: u16 = rng.gen_range(1024..65535);
@@ -414,8 +504,8 @@ impl Stack {
local_addr, local_addr,
remote_addr, remote_addr,
0, 0,
tcp_packet.get_sequence() + 1, tcp_packet.get_sequence() + tcp_packet.payload().len() as u32 + 1, // +1 because of SYN flag set
tcp::TcpFlags::RST, tcp::TcpFlags::RST | tcp::TcpFlags::ACK,
None, None,
); );
shared.tun[0].try_send(&buf).unwrap(); shared.tun[0].try_send(&buf).unwrap();
@@ -426,8 +516,8 @@ impl Stack {
local_addr, local_addr,
remote_addr, remote_addr,
tcp_packet.get_acknowledgement(), tcp_packet.get_acknowledgement(),
0, tcp_packet.get_sequence() + tcp_packet.payload().len() as u32,
tcp::TcpFlags::RST, tcp::TcpFlags::RST | tcp::TcpFlags::ACK,
None, None,
); );
shared.tun[0].try_send(&buf).unwrap(); shared.tun[0].try_send(&buf).unwrap();
@@ -436,7 +526,7 @@ impl Stack {
tuple = tuples_purge.recv() => { tuple = tuples_purge.recv() => {
let tuple = tuple.unwrap(); let tuple = tuple.unwrap();
tuples.remove(&tuple); tuples.remove(&tuple);
trace!("Removed cached tuple"); trace!("Removed cached tuple: {:?}", tuple);
} }
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "phantun" name = "phantun"
version = "0.2.5" version = "0.3.1"
edition = "2021" edition = "2021"
authors = ["Datong Sun <dndx@idndx.com>"] authors = ["Datong Sun <dndx@idndx.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -13,8 +13,9 @@ Layer 3 & Layer 4 (NAPT) firewalls/NATs.
[dependencies] [dependencies]
clap = { version = "3.0", features = ["cargo"] } clap = { version = "3.0", features = ["cargo"] }
socket2 = { version = "0.4", features = ["all"] } socket2 = { version = "0.4", features = ["all"] }
fake-tcp = "0.2" fake-tcp = { path = "../fake-tcp", version = "0.3" }
tokio = { version = "1.14", features = ["full"] } tokio = { version = "1.14", features = ["full"] }
tokio-util = "0.7"
log = "0.4" log = "0.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
tokio-tun = "0.5" tokio-tun = "0.5"

View File

@@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2014-2021 The Rust Project Developers Copyright 2021-2022 Datong Sun (dndx@idndx.com)
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2014-2021 The Rust Project Developers Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

View File

@@ -1,44 +1,23 @@
use clap::{crate_version, App, Arg}; use clap::{crate_version, Arg, Command};
use fake_tcp::packet::MAX_PACKET_LEN; use fake_tcp::packet::MAX_PACKET_LEN;
use fake_tcp::{Socket, Stack}; use fake_tcp::{Socket, Stack};
use log::{debug, error, info}; use log::{debug, error, info};
use phantun::utils::new_udp_reuseport;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto;
use std::net::{Ipv4Addr, SocketAddr}; use std::net::{Ipv4Addr, SocketAddr};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use tokio::sync::{Notify, RwLock};
use tokio::net::UdpSocket;
use tokio::sync::RwLock;
use tokio::time; use tokio::time;
use tokio_tun::TunBuilder; use tokio_tun::TunBuilder;
use tokio_util::sync::CancellationToken;
const UDP_TTL: Duration = Duration::from_secs(180); use phantun::UDP_TTL;
fn new_udp_reuseport(addr: SocketAddr) -> UdpSocket {
let udp_sock = socket2::Socket::new(
if addr.is_ipv4() {
socket2::Domain::IPV4
} else {
socket2::Domain::IPV6
},
socket2::Type::DGRAM,
None,
)
.unwrap();
udp_sock.set_reuse_port(true).unwrap();
// from tokio-rs/mio/blob/master/src/sys/unix/net.rs
udp_sock.set_cloexec(true).unwrap();
udp_sock.set_nonblocking(true).unwrap();
udp_sock.bind(&socket2::SockAddr::from(addr)).unwrap();
let udp_sock: std::net::UdpSocket = udp_sock.into();
udp_sock.try_into().unwrap()
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
let matches = App::new("Phantun Client") let matches = Command::new("Phantun Client")
.version(crate_version!()) .version(crate_version!())
.author("Datong Sun (github.com/dndx)") .author("Datong Sun (github.com/dndx)")
.arg( .arg(
@@ -119,6 +98,9 @@ async fn main() {
.parse() .parse()
.expect("bad peer address for Tun interface"); .expect("bad peer address for Tun interface");
let num_cpus = num_cpus::get();
info!("{} cores available", num_cpus);
let tun = TunBuilder::new() let tun = TunBuilder::new()
.name(matches.value_of("tun").unwrap()) // if name is empty, then it is set by kernel. .name(matches.value_of("tun").unwrap()) // if name is empty, then it is set by kernel.
.tap(false) // false (default): TUN, true: TAP. .tap(false) // false (default): TUN, true: TAP.
@@ -126,7 +108,7 @@ async fn main() {
.up() // or set it up manually using `sudo ip link set <tun-name> up`. .up() // or set it up manually using `sudo ip link set <tun-name> up`.
.address(tun_local) .address(tun_local)
.destination(tun_peer) .destination(tun_peer)
.try_build_mq(num_cpus::get()) .try_build_mq(num_cpus)
.unwrap(); .unwrap();
info!("Created TUN device {}", tun[0].name()); info!("Created TUN device {}", tun[0].name());
@@ -168,52 +150,85 @@ async fn main() {
assert!(connections.write().await.insert(addr, sock.clone()).is_none()); assert!(connections.write().await.insert(addr, sock.clone()).is_none());
debug!("inserted fake TCP socket into connection table"); debug!("inserted fake TCP socket into connection table");
let connections = connections.clone();
// spawn "fastpath" UDP socket and task, this will offload main task // spawn "fastpath" UDP socket and task, this will offload main task
// from forwarding UDP packets // from forwarding UDP packets
tokio::spawn(async move {
let mut buf_udp = [0u8; MAX_PACKET_LEN];
let mut buf_tcp = [0u8; MAX_PACKET_LEN];
let udp_sock = new_udp_reuseport(local_addr);
udp_sock.connect(addr).await.unwrap();
let packet_received = Arc::new(Notify::new());
let quit = CancellationToken::new();
for i in 0..num_cpus {
let sock = sock.clone();
let quit = quit.clone();
let packet_received = packet_received.clone();
tokio::spawn(async move {
let mut buf_udp = [0u8; MAX_PACKET_LEN];
let mut buf_tcp = [0u8; MAX_PACKET_LEN];
let udp_sock = new_udp_reuseport(local_addr);
udp_sock.connect(addr).await.unwrap();
loop {
tokio::select! {
Ok(size) = udp_sock.recv(&mut buf_udp) => {
if sock.send(&buf_udp[..size]).await.is_none() {
debug!("removed fake TCP socket from connections table");
quit.cancel();
return;
}
packet_received.notify_one();
},
res = sock.recv(&mut buf_tcp) => {
match res {
Some(size) => {
if size > 0 {
if let Err(e) = udp_sock.send(&buf_tcp[..size]).await {
error!("Unable to send UDP packet to {}: {}, closing connection", e, addr);
quit.cancel();
return;
}
}
},
None => {
debug!("removed fake TCP socket from connections table");
quit.cancel();
return;
},
}
packet_received.notify_one();
},
_ = quit.cancelled() => {
debug!("worker {} terminated", i);
return;
},
};
}
});
}
let connections = connections.clone();
tokio::spawn(async move {
loop { loop {
let read_timeout = time::sleep(UDP_TTL); let read_timeout = time::sleep(UDP_TTL);
let packet_received_fut = packet_received.notified();
tokio::select! { tokio::select! {
Ok(size) = udp_sock.recv(&mut buf_udp) => {
if sock.send(&buf_udp[..size]).await.is_none() {
connections.write().await.remove(&addr);
debug!("removed fake TCP socket from connections table");
return;
}
},
res = sock.recv(&mut buf_tcp) => {
match res {
Some(size) => {
if size > 0 {
if let Err(e) = udp_sock.send(&buf_tcp[..size]).await {
connections.write().await.remove(&addr);
error!("Unable to send UDP packet to {}: {}, closing connection", e, addr);
return;
}
}
},
None => {
connections.write().await.remove(&addr);
debug!("removed fake TCP socket from connections table");
return;
},
}
},
_ = read_timeout => { _ = read_timeout => {
info!("No traffic seen in the last {:?}, closing connection", UDP_TTL); info!("No traffic seen in the last {:?}, closing connection", UDP_TTL);
connections.write().await.remove(&addr); connections.write().await.remove(&addr);
debug!("removed fake TCP socket from connections table"); debug!("removed fake TCP socket from connections table");
quit.cancel();
return; return;
} },
}; _ = quit.cancelled() => {
connections.write().await.remove(&addr);
debug!("removed fake TCP socket from connections table");
return;
},
_ = packet_received_fut => {},
}
} }
}); });
}, },

View File

@@ -1,18 +1,23 @@
use clap::{crate_version, App, Arg}; use clap::{crate_version, Arg, Command};
use fake_tcp::packet::MAX_PACKET_LEN; use fake_tcp::packet::MAX_PACKET_LEN;
use fake_tcp::Stack; use fake_tcp::Stack;
use log::{error, info}; use log::{debug, error, info};
use phantun::utils::new_udp_reuseport;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::sync::Arc;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use tokio::time::{self, Duration}; use tokio::sync::Notify;
use tokio::time;
use tokio_tun::TunBuilder; use tokio_tun::TunBuilder;
const UDP_TTL: Duration = Duration::from_secs(180); use tokio_util::sync::CancellationToken;
use phantun::UDP_TTL;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
let matches = App::new("Phantun Server") let matches = Command::new("Phantun Server")
.version(crate_version!()) .version(crate_version!())
.author("Datong Sun (github.com/dndx)") .author("Datong Sun (github.com/dndx)")
.arg( .arg(
@@ -88,6 +93,9 @@ async fn main() {
.parse() .parse()
.expect("bad peer address for Tun interface"); .expect("bad peer address for Tun interface");
let num_cpus = num_cpus::get();
info!("{} cores available", num_cpus);
let tun = TunBuilder::new() let tun = TunBuilder::new()
.name(matches.value_of("tun").unwrap()) // if name is empty, then it is set by kernel. .name(matches.value_of("tun").unwrap()) // if name is empty, then it is set by kernel.
.tap(false) // false (default): TUN, true: TAP. .tap(false) // false (default): TUN, true: TAP.
@@ -95,7 +103,7 @@ async fn main() {
.up() // or set it up manually using `sudo ip link set <tun-name> up`. .up() // or set it up manually using `sudo ip link set <tun-name> up`.
.address(tun_local) .address(tun_local)
.destination(tun_peer) .destination(tun_peer)
.try_build_mq(num_cpus::get()) .try_build_mq(num_cpus)
.unwrap(); .unwrap();
info!("Created TUN device {}", tun[0].name()); info!("Created TUN device {}", tun[0].name());
@@ -110,46 +118,82 @@ async fn main() {
let mut buf_tcp = [0u8; MAX_PACKET_LEN]; let mut buf_tcp = [0u8; MAX_PACKET_LEN];
loop { loop {
let sock = stack.accept().await; let sock = Arc::new(stack.accept().await);
info!("New connection: {}", sock); info!("New connection: {}", sock);
tokio::spawn(async move { let packet_received = Arc::new(Notify::new());
let udp_sock = UdpSocket::bind(if remote_addr.is_ipv4() { let quit = CancellationToken::new();
"0.0.0.0:0" let udp_sock = UdpSocket::bind(if remote_addr.is_ipv4() {
} else { "0.0.0.0:0"
"[::]:0" } else {
}) "[::]:0"
.await })
.unwrap(); .await
udp_sock.connect(remote_addr).await.unwrap(); .unwrap();
let local_addr = udp_sock.local_addr().unwrap();
drop(udp_sock);
for i in 0..num_cpus {
let sock = sock.clone();
let quit = quit.clone();
let packet_received = packet_received.clone();
let udp_sock = new_udp_reuseport(local_addr);
tokio::spawn(async move {
udp_sock.connect(remote_addr).await.unwrap();
loop {
tokio::select! {
Ok(size) = udp_sock.recv(&mut buf_udp) => {
if sock.send(&buf_udp[..size]).await.is_none() {
quit.cancel();
return;
}
packet_received.notify_one();
},
res = sock.recv(&mut buf_tcp) => {
match res {
Some(size) => {
if size > 0 {
if let Err(e) = udp_sock.send(&buf_tcp[..size]).await {
error!("Unable to send UDP packet to {}: {}, closing connection", e, remote_addr);
quit.cancel();
return;
}
}
},
None => {
quit.cancel();
return;
},
}
packet_received.notify_one();
},
_ = quit.cancelled() => {
debug!("worker {} terminated", i);
return;
},
};
}
});
}
tokio::spawn(async move {
loop { loop {
let read_timeout = time::sleep(UDP_TTL); let read_timeout = time::sleep(UDP_TTL);
let packet_received_fut = packet_received.notified();
tokio::select! { tokio::select! {
Ok(size) = udp_sock.recv(&mut buf_udp) => {
if sock.send(&buf_udp[..size]).await.is_none() {
return;
}
},
res = sock.recv(&mut buf_tcp) => {
match res {
Some(size) => {
if size > 0 {
if let Err(e) = udp_sock.send(&buf_tcp[..size]).await {
error!("Unable to send UDP packet to {}: {}, closing connection", e, remote_addr);
return;
}
}
},
None => { return; },
}
},
_ = read_timeout => { _ = read_timeout => {
info!("No traffic seen in the last {:?}, closing connection", UDP_TTL); info!("No traffic seen in the last {:?}, closing connection", UDP_TTL);
quit.cancel();
return; return;
} },
}; _ = packet_received_fut => {},
}
} }
}); });
} }

5
phantun/src/lib.rs Normal file
View File

@@ -0,0 +1,5 @@
use std::time::Duration;
pub mod utils;
pub const UDP_TTL: Duration = Duration::from_secs(180);

22
phantun/src/utils.rs Normal file
View File

@@ -0,0 +1,22 @@
use std::net::SocketAddr;
use tokio::net::UdpSocket;
pub fn new_udp_reuseport(local_addr: SocketAddr) -> UdpSocket {
let udp_sock = socket2::Socket::new(
if local_addr.is_ipv4() {
socket2::Domain::IPV4
} else {
socket2::Domain::IPV6
},
socket2::Type::DGRAM,
None,
)
.unwrap();
udp_sock.set_reuse_port(true).unwrap();
// from tokio-rs/mio/blob/master/src/sys/unix/net.rs
udp_sock.set_cloexec(true).unwrap();
udp_sock.set_nonblocking(true).unwrap();
udp_sock.bind(&socket2::SockAddr::from(local_addr)).unwrap();
let udp_sock: std::net::UdpSocket = udp_sock.into();
udp_sock.try_into().unwrap()
}