mirror of
https://github.com/dndx/phantun.git
synced 2025-04-04 02:59:29 +08:00
Compare commits
77 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
869c79422f | ||
|
201da45ee8 | ||
|
333c6dd059 | ||
|
62f0278c1a | ||
|
f436325d23 | ||
|
028a32d197 | ||
|
e452e40edd | ||
|
253ce54554 | ||
|
34d2350d1c | ||
|
6955a1eb4c | ||
|
e86c5c5c50 | ||
|
60f24d2563 | ||
|
8c7f4e98b3 | ||
|
90b93370ce | ||
|
792ee46ec4 | ||
|
f5cb4b1220 | ||
|
8a0ec729e2 | ||
|
78dd7c13b1 | ||
|
b58d58956b | ||
|
310bb17516 | ||
|
632132b75a | ||
|
48a0399f59 | ||
|
590a048b5b | ||
|
3fa8f86379 | ||
|
805bf80cd8 | ||
|
aec3bcdeda | ||
|
9b1d3c0124 | ||
|
6c42f02b28 | ||
|
ee0bce0a96 | ||
|
1f11d618e0 | ||
|
d7913c1407 | ||
|
ee7ee5d5f9 | ||
|
af3a9061a1 | ||
|
7c98012a67 | ||
|
b674268863 | ||
|
b40ca10cc1 | ||
|
30f0a1118b | ||
|
fd607bc72a | ||
|
939e4aa94e | ||
|
7bcfada87b | ||
|
fe18a49d40 | ||
|
b707c5bd12 | ||
|
6af7757456 | ||
|
f374ac8081 | ||
|
50346c1ba0 | ||
|
f649c79656 | ||
|
c91bda7e6a | ||
|
00a308a005 | ||
|
9ff691d063 | ||
|
b5e79653f0 | ||
|
f496a7919b | ||
|
bf6b9bc2ff | ||
|
47b9037968 | ||
|
c2341b6662 | ||
|
a3eff42453 | ||
|
87a42a1e23 | ||
|
851750b13d | ||
|
b89b683bb2 | ||
|
838cfa6738 | ||
|
827530f62c | ||
|
245cb9c7f4 | ||
|
85555f2a34 | ||
|
74183071f1 | ||
|
2f4eaafccd | ||
|
1e3b632413 | ||
|
99bff568f6 | ||
|
91ad2c03a1 | ||
|
581d80d08c | ||
|
55da4d6a62 | ||
|
bb859be6b6 | ||
|
8d315ea4e7 | ||
|
21eabe8b82 | ||
|
8a74b31c6e | ||
|
ca14ba457f | ||
|
33a0cfe567 | ||
|
95dfd8ab54 | ||
|
1c35635091 |
30
.github/workflows/docker.yml
vendored
Normal file
30
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: Docker image build
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
tags: phantun
|
||||
platforms: linux/amd64
|
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -25,13 +25,9 @@ jobs:
|
||||
- arm-unknown-linux-musleabihf
|
||||
- aarch64-unknown-linux-gnu
|
||||
- aarch64-unknown-linux-musl
|
||||
- mips-unknown-linux-gnu
|
||||
- mips-unknown-linux-musl
|
||||
- mipsel-unknown-linux-gnu
|
||||
- mipsel-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
@ -50,7 +46,7 @@ jobs:
|
||||
zip phantun_${{ matrix.target }}.zip phantun_client phantun_server
|
||||
|
||||
- name: Upload Github Assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: target/${{ matrix.target }}/release/*.zip
|
||||
prerelease: ${{ contains(github.ref, '-') }}
|
||||
|
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
|
1101
Cargo.lock
generated
Normal file
1101
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
||||
[workspace]
|
||||
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"fake-tcp",
|
||||
"phantun",
|
||||
|
@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
|
||||
Copyright 2021-2024 Datong Sun (dndx@idndx.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
|
||||
Copyright (c) 2021-2024 Datong Sun (dndx@idndx.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
116
README.md
116
README.md
@ -2,6 +2,9 @@
|
||||
|
||||
A lightweight and fast UDP to TCP obfuscator.
|
||||
|
||||

|
||||

|
||||
|
||||
Table of Contents
|
||||
=================
|
||||
|
||||
@ -32,7 +35,7 @@ Table of Contents
|
||||
|
||||
# Latest release
|
||||
|
||||
[v0.2.5](https://github.com/dndx/phantun/releases/tag/v0.2.5)
|
||||
[v0.7.0](https://github.com/dndx/phantun/releases/tag/v0.7.0)
|
||||
|
||||
# Overview
|
||||
|
||||
@ -52,6 +55,11 @@ connection from the perspective of firewalls/NAT devices.
|
||||
Phantun means Phantom TUN, as it is an obfuscator for UDP traffic that does just enough work
|
||||
to make it pass through stateful firewall/NATs as TCP packets.
|
||||
|
||||
Phantun is written in 100% safe Rust. It has been optimized extensively to scale well on multi-core
|
||||
systems and has no issue saturating all available CPU resources on a fast connection.
|
||||
See the [Performance](#performance) section for benchmarking results.
|
||||
|
||||

|
||||

|
||||
|
||||
# Usage
|
||||
@ -64,30 +72,32 @@ It is also assumed that **Phantun Client** listens for incoming UDP packets at
|
||||
`127.0.0.1:1234` (the `--local` option for client) and connects to Phantun Server at `10.0.0.1:4567`
|
||||
(the `--remote` option for client).
|
||||
|
||||
Phantun creates TUN interface for both the Client and Server. For Client, Phantun assigns itself the IP address
|
||||
`192.168.200.2` by default and for Server, it assigns `192.168.201.2` by default. Therefore, your Kernel must have
|
||||
`net.ipv4.ip_forward` enabled and setup appropriate iptables rules for NAT between your physical
|
||||
NIC address and Phantun's TUN interface address.
|
||||
Phantun creates TUN interface for both the Client and Server. For **Client**, Phantun assigns itself the IP address
|
||||
`192.168.200.2` and `fcc8::2` by default.
|
||||
For **Server**, it assigns `192.168.201.2` and `fcc9::2` by default. Therefore, your Kernel must have
|
||||
IPv4/IPv6 forwarding enabled and setup appropriate iptables/nftables rules for NAT between your physical
|
||||
NIC address and Phantun's Tun interface address.
|
||||
|
||||
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.
|
||||
|
||||
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`/`fcc8::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
|
||||
leaves the NIC.
|
||||
|
||||
Phantun Server is like a server with private IP address (`192.168.201.2`) behind a router.
|
||||
Phantun Server is like a server with private IP address (`192.168.201.2`/`fcc9::2`) behind a router.
|
||||
In order to access it from the Internet, you need to `DNAT` it's listening port on the router
|
||||
and change the destination IP address to where the server is listening for incoming connections.
|
||||
|
||||
In those cases, the machine/iptables running Phantun acts as the "router" that allows Phantun
|
||||
to communicate with outside using it's private IP addresses.
|
||||
|
||||
As of Phantun v0.2.2, IPv6 support for UDP endpoints has been added, however Fake TCP IPv6 support
|
||||
has not been finished yet. To specify an IPv6 address, use the following format: `[::1]:1234` with
|
||||
the command line options.
|
||||
As of Phantun v0.4.1, IPv6 is fully supported for both TCP and UDP sides.
|
||||
To specify an IPv6 address, use the following format: `[::1]:1234` with
|
||||
the command line options. Resolving AAAA record is also supported. Please run the program
|
||||
with `-h` to see detailed options on how to control the IPv6 behavior.
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
@ -95,6 +105,12 @@ the command line options.
|
||||
|
||||
Edit `/etc/sysctl.conf`, add `net.ipv4.ip_forward=1` and run `sudo sysctl -p /etc/sysctl.conf`.
|
||||
|
||||
<details>
|
||||
<summary>IPv6 specific config</summary>
|
||||
|
||||
`net.ipv6.conf.all.forwarding=1` will need to be set as well.
|
||||
</details>
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
## 2. Add required firewall rules
|
||||
@ -120,12 +136,16 @@ table inet nat {
|
||||
}
|
||||
```
|
||||
|
||||
Note: The above rule uses `inet` as the table family type, so it is compatible with
|
||||
both IPv4 and IPv6 usage.
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
#### Using iptables
|
||||
|
||||
```
|
||||
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
```
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
@ -142,10 +162,11 @@ actual TCP port number used by Phantun server
|
||||
#### Using nftables
|
||||
|
||||
```
|
||||
table ip nat {
|
||||
table inet nat {
|
||||
chain prerouting {
|
||||
type nat hook prerouting priority dstnat; policy accept;
|
||||
iif eth0 tcp dport 4567 dnat to 192.168.201.2
|
||||
iif eth0 tcp dport 4567 dnat ip to 192.168.201.2
|
||||
iif eth0 tcp dport 4567 dnat ip6 to fcc9::2
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -156,6 +177,7 @@ table ip nat {
|
||||
|
||||
```
|
||||
iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination 192.168.201.2
|
||||
ip6tables -t nat -A PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination fcc9::2
|
||||
```
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
@ -194,6 +216,10 @@ Or use host name with `--remote`:
|
||||
RUST_LOG=info /usr/local/bin/phantun_server --local 4567 --remote example.com:1234
|
||||
```
|
||||
|
||||
Note: Server by default assigns both IPv4 and IPv6 private address to the Tun interface.
|
||||
If you do not wish to use IPv6, you can simply skip creating the IPv6 DNAT rule above and
|
||||
the presence of IPv6 address on the Tun interface should have no side effect to the server.
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
### Client
|
||||
@ -211,25 +237,37 @@ Or use host name with `--remote`:
|
||||
RUST_LOG=info /usr/local/bin/phantun_client --local 127.0.0.1:1234 --remote example.com:4567
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>IPv6 specific config</summary>
|
||||
|
||||
```
|
||||
RUST_LOG=info /usr/local/bin/phantun_client --local 127.0.0.1:1234 --remote [fdxx::1234]:4567
|
||||
```
|
||||
|
||||
Domain name with AAAA record is also supported.
|
||||
</details>
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
# MTU overhead
|
||||
|
||||
Phantun aims to keep tunneling overhead to the minimum. The overhead compared to a plain UDP packet
|
||||
is the following:
|
||||
is the following (using IPv4 below as an example):
|
||||
|
||||
Standard UDP packet: 20 byte IP header + 8 byte UDP header = 28 bytes
|
||||
**Standard UDP packet:** `20 byte IP header + 8 byte UDP header = 28 bytes`
|
||||
|
||||
Phantun obfuscated UDP packet: 20 byte IP header + 20 byte TCP header = 40 bytes
|
||||
**Obfuscated packet:** `20 byte IP header + 20 byte TCP header = 40 bytes`
|
||||
|
||||
|
||||
Note that Phantun does not add any additional header other than IP and TCP headers in order to pass through
|
||||
stateful packet inspection!
|
||||
|
||||
Phantun's additional overhead: 12 bytes. I other words, when using Phantun, the usable payload for
|
||||
Phantun's additional overhead: `12 bytes`. In other words, when using Phantun, the usable payload for
|
||||
UDP packet is reduced by 12 bytes. This is the minimum overhead possible when doing such kind
|
||||
of obfuscation.
|
||||
|
||||

|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
## MTU calculation for WireGuard
|
||||
@ -237,14 +275,25 @@ of obfuscation.
|
||||
For people who use Phantun to tunnel [WireGuard®](https://www.wireguard.com) UDP packets, here are some guidelines on figuring
|
||||
out the correct MTU to use for your WireGuard interface.
|
||||
|
||||
WireGuard MTU = Interface MTU - IP header (20 bytes) - TCP header (20 bytes) - WireGuard overhead (32 bytes)
|
||||
```
|
||||
WireGuard MTU = Interface MTU - IPv4 header (20 bytes) - TCP header (20 bytes) - WireGuard overhead (32 bytes)
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
WireGuard MTU = Interface MTU - IPv6 header (40 bytes) - TCP header (20 bytes) - WireGuard overhead (32 bytes)
|
||||
```
|
||||
|
||||
For example, for a Ethernet interface with 1500 bytes MTU, the WireGuard interface MTU should be set as:
|
||||
|
||||
1500 - 20 - 20 - 32 = 1428 bytes
|
||||
IPv4: `1500 - 20 - 20 - 32 = 1428 bytes`
|
||||
IPv6: `1500 - 40 - 20 - 32 = 1408 bytes`
|
||||
|
||||
The resulted Phantun TCP data packet will be 1500 bytes which does not exceed the
|
||||
interface MTU of 1500.
|
||||
interface MTU of 1500. Please note it is strongly recommended to use the same interface
|
||||
MTU for both ends of a WireGuard tunnel, or unexpected packet loss may occur and these issues are
|
||||
generally very hard to troubleshoot.
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
@ -264,19 +313,28 @@ For users who wish to use `fake-tcp` library inside their own project, refer to
|
||||
|
||||
# Performance
|
||||
|
||||
Performance was tested on AWS t3.xlarge instance with 4 vCPUs and 5 Gb/s NIC. WireGuard was used
|
||||
for tunneling TCP/UDP traffic between two test instances and MTU has been tuned to avoid fragmentation.
|
||||
Performance was tested on 2 AWS `t4g.xlarge` instances with 4 vCPUs and 5 Gb/s NIC over LAN. `nftables` was used to redirect
|
||||
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) |
|
||||
|-----------------|-------------|---------------------|---------------------------------------------------------------------------|
|
||||
| iperf3 -c IP -R | 1.56 Gbit/s | 540 Mbit/s | 369 Mbit/s |
|
||||
| iperf3 -c IP | 1.71 Gbit/s | 519 Mbit/s | 312 Mbit/s |
|
||||
Phantun `v0.3.2` and `udp2raw_arm_asm_aes` `20200818.0` was used. These were the latest release of both projects as of Apr 2022.
|
||||
|
||||
Test command: `iperf3 -c <IP> -p <PORT> -R -u -l 1400 -b 1000m -t 30 -P 5`
|
||||
|
||||
| Mode | Send Speed | Receive Speed | Overall CPU Usage |
|
||||
|---------------------------------------------------------------------------------|----------------|----------------|-----------------------------------------------------|
|
||||
| Direct (1 stream) | 3.00 Gbits/sec | 2.37 Gbits/sec | 25% (1 core at 100%) |
|
||||
| Phantun (1 stream) | 1.30 Gbits/sec | 1.20 Gbits/sec | 60% (1 core at 100%, 3 cores at 50%) |
|
||||
| udp2raw (`cipher-mode=none` `auth-mode=none` `disable-anti-replay`) (1 stream) | 1.30 Gbits/sec | 715 Mbits/sec | 40% (1 core at 100%, 1 core at 50%, 2 cores idling) |
|
||||
| Direct connection (5 streams) | 5.00 Gbits/sec | 3.64 Gbits/sec | 25% (1 core at 100%) |
|
||||
| Phantun (5 streams) | 5.00 Gbits/sec | 2.38 Gbits/sec | 95% (all cores utilized) |
|
||||
| udp2raw (`cipher-mode=none` `auth-mode=none` `disable-anti-replay`) (5 streams) | 5.00 Gbits/sec | 770 Mbits/sec | 50% (2 cores at 100%) |
|
||||
|
||||
Writeup on some of the techniques used in Phantun to achieve this performance result: [Writing Highly Efficient UDP Server in Rust](https://idndx.com/writing-highly-efficient-udp-server-in-rust/).
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
# Future plans
|
||||
|
||||
* IPv6 support for fake-tcp
|
||||
* Load balancing a single UDP stream into multiple TCP streams
|
||||
* Integration tests
|
||||
* Auto insertion/removal of required firewall rules
|
||||
@ -300,17 +358,17 @@ Here is a quick overview of comparison between those two to help you choose:
|
||||
| UDP over UDP obfuscation | ❌ | ✅ |
|
||||
| Multi-threaded | ✅ | ❌ |
|
||||
| Throughput | Better | Good |
|
||||
| Raw IP mode | TUN interface | Raw sockets + BPF |
|
||||
| Layer 3 mode | TUN interface | Raw sockets + BPF |
|
||||
| Tunneling MTU overhead | 12 bytes | 44 bytes |
|
||||
| Seprate TCP connections for each UDP connection | Client/Server | Server only |
|
||||
| Anti-replay, encryption | ❌ | ✅ |
|
||||
| IPv6 | UDP only | ✅ |
|
||||
| IPv6 | ✅ | ✅ |
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
# License
|
||||
|
||||
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
|
||||
Copyright 2021-2024 Datong Sun (dndx@idndx.com)
|
||||
|
||||
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
|
||||
|
2
docker/.dockerignore
Normal file
2
docker/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
README.md
|
||||
docker-compose.yml
|
33
docker/Dockerfile
Normal file
33
docker/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
#
|
||||
# Dockerfile for phantun
|
||||
#
|
||||
|
||||
#
|
||||
# Build stage
|
||||
#
|
||||
FROM rust:latest AS builder
|
||||
|
||||
COPY . /phantun
|
||||
|
||||
RUN cd phantun \
|
||||
&& cargo build --release \
|
||||
&& strip target/release/server target/release/client \
|
||||
&& install target/release/server /usr/local/bin/phantun-server \
|
||||
&& install target/release/client /usr/local/bin/phantun-client \
|
||||
&& cd - \
|
||||
&& rm -r phantun
|
||||
|
||||
#
|
||||
# Runtime stage
|
||||
#
|
||||
FROM debian:latest
|
||||
|
||||
COPY --from=builder /usr/local/bin/phantun-server /usr/local/bin/
|
||||
COPY --from=builder /usr/local/bin/phantun-client /usr/local/bin/
|
||||
COPY docker/phantun.sh /usr/local/bin/
|
||||
|
||||
ENV USE_IPTABLES_NFT_BACKEND=0
|
||||
ENV RUST_LOG=INFO
|
||||
|
||||
ENTRYPOINT ["phantun.sh"]
|
||||
CMD ["phantun-server", "--help"]
|
11
docker/README.md
Normal file
11
docker/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# phantun (docker)
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
docker build -t phantun -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
It is recommended to use docker-compose, see [docker-compose.yml](docker-compose.yml) for details.
|
26
docker/docker-compose.yml
Normal file
26
docker/docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
phantun-server:
|
||||
image: phantun
|
||||
container_name: phantun-server
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
privileged: true
|
||||
environment:
|
||||
USE_IPTABLES_NFT_BACKEND: 0
|
||||
RUST_LOG: INFO
|
||||
command: >
|
||||
phantun-server --local 1985 --remote 127.0.0.1:1984 --ipv4-only
|
||||
|
||||
phantun-client:
|
||||
image: phantun
|
||||
container_name: phantun-client
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
privileged: true
|
||||
environment:
|
||||
USE_IPTABLES_NFT_BACKEND: 0
|
||||
RUST_LOG: INFO
|
||||
command: >
|
||||
phantun-client --local 127.0.0.1:1984 --remote 11.22.33.44:1985 --ipv4-only
|
209
docker/phantun.sh
Executable file
209
docker/phantun.sh
Executable file
@ -0,0 +1,209 @@
|
||||
#!/bin/sh
|
||||
|
||||
# alias settings must be global, and must be defined before the function being called with the alias
|
||||
if [ "$USE_IPTABLES_NFT_BACKEND" = 1 ]; then
|
||||
alias iptables=iptables-nft
|
||||
alias iptables-save=iptables-nft-save
|
||||
alias ip6tables=ip6tables-nft
|
||||
alias ip6tables-save=ip6tables-nft-save
|
||||
fi
|
||||
|
||||
info() {
|
||||
local green='\e[0;32m'
|
||||
local clear='\e[0m'
|
||||
local time=$(date '+%Y-%m-%d %T')
|
||||
printf "${green}[${time}] [INFO]: ${clear}%s\n" "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
local yellow='\e[1;33m'
|
||||
local clear='\e[0m'
|
||||
local time=$(date '+%Y-%m-%d %T')
|
||||
printf "${yellow}[${time}] [WARN]: ${clear}%s\n" "$*" >&2
|
||||
}
|
||||
|
||||
error() {
|
||||
local red='\e[0;31m'
|
||||
local clear='\e[0m'
|
||||
local time=$(date '+%Y-%m-%d %T')
|
||||
printf "${red}[${time}] [ERROR]: ${clear}%s\n" "$*" >&2
|
||||
}
|
||||
|
||||
_get_default_iface() {
|
||||
ip -4 route show default | awk -F 'dev' '{print $2}' | awk '{print $1}'
|
||||
}
|
||||
|
||||
_get_default6_iface() {
|
||||
ip -6 route show default | awk -F 'dev' '{print $2}' | awk '{print $1}'
|
||||
}
|
||||
|
||||
_get_addr_by_iface() {
|
||||
ip -4 addr show dev "$1" | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | head -1
|
||||
}
|
||||
|
||||
_get_addr6_by_iface() {
|
||||
ip -6 addr show dev "$1" | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | head -1
|
||||
}
|
||||
|
||||
_check_rule_by_comment() {
|
||||
iptables-save | grep -q "$1"
|
||||
}
|
||||
|
||||
_check_rule6_by_comment() {
|
||||
ip6tables-save | grep -q "$1"
|
||||
}
|
||||
|
||||
_is_server_mode() {
|
||||
[ "$1" = "phantun-server" ]
|
||||
}
|
||||
|
||||
_is_ipv4_only() {
|
||||
case "$@" in
|
||||
*-4*|*--ipv4-only*)
|
||||
return 0
|
||||
;;
|
||||
*\ -4*|*\ --ipv4-only*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
_get_tun_from_args() {
|
||||
local tun=$(echo "$@" | awk -F '--tun' '{print $2}' | awk '{print $1}')
|
||||
echo ${tun:=tun0}
|
||||
}
|
||||
|
||||
_get_peer_from_args() {
|
||||
local peer=$(echo "$@" | awk -F '--tun-peer' '{print $2}' | awk '{print $1}')
|
||||
_is_server_mode "$1" && echo ${peer:=192.168.201.2} || echo ${peer:=192.168.200.2}
|
||||
}
|
||||
|
||||
_get_peer6_from_args() {
|
||||
local peer=$(echo "$@" | awk -F '--tun-peer6' '{print $2}' | awk '{print $1}')
|
||||
_is_server_mode "$1" && echo ${peer:=fcc9::2} || echo ${peer:=fcc8::2}
|
||||
}
|
||||
|
||||
_get_port_from_args() {
|
||||
local value=$(echo "$@" | awk -F '-l|--local' '{print $2}' | awk '{print $1}')
|
||||
_is_server_mode "$1" && echo $value || echo $value | awk -F ':' '{print $2}'
|
||||
}
|
||||
|
||||
_iptables() {
|
||||
iptables -w 10 "$@"
|
||||
}
|
||||
|
||||
_ip6tables() {
|
||||
ip6tables -w 10 "$@"
|
||||
}
|
||||
|
||||
apply_sysctl() {
|
||||
info "apply sysctl: $(sysctl -w net.ipv4.ip_forward=1)"
|
||||
! _is_ipv4_only "$@" || return
|
||||
info "apply sysctl: $(sysctl -w net.ipv6.conf.all.forwarding=1)"
|
||||
}
|
||||
|
||||
apply_iptables() {
|
||||
local interface=$(_get_default_iface)
|
||||
local address=$(_get_addr_by_iface "${interface}")
|
||||
local tun=$(_get_tun_from_args "$@")
|
||||
local peer=$(_get_peer_from_args "$@")
|
||||
local port=$(_get_port_from_args "$@")
|
||||
local comment="phantun_${tun}_${port}"
|
||||
|
||||
if _check_rule_by_comment "${comment}"; then
|
||||
warn "iptables rules already exist, maybe needs to check."
|
||||
else
|
||||
_iptables -A FORWARD -i $tun -j ACCEPT -m comment --comment "${comment}" || error "iptables filter rule add failed."
|
||||
_iptables -A FORWARD -o $tun -j ACCEPT -m comment --comment "${comment}" || error "iptables filter rule add failed."
|
||||
if _is_server_mode "$1"; then
|
||||
info "iptables DNAT rule added: [${comment}]: ${interface} -> ${tun}, ${address} -> ${peer}"
|
||||
_iptables -t nat -A PREROUTING -p tcp -i $interface --dport $port -j DNAT --to-destination $peer \
|
||||
-m comment --comment "${comment}" || error "iptables DNAT rule add failed."
|
||||
else
|
||||
info "iptables SNAT rule added: [${comment}]: ${tun} -> ${interface}, ${peer} -> ${address}"
|
||||
_iptables -t nat -A POSTROUTING -s $peer -o $interface -j SNAT --to-source $address \
|
||||
-m comment --comment "${comment}" || error "iptables SNAT rule add failed."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
apply_ip6tables() {
|
||||
! _is_ipv4_only "$@" || return
|
||||
|
||||
local interface=$(_get_default6_iface)
|
||||
local address=$(_get_addr6_by_iface "${interface}")
|
||||
local tun=$(_get_tun_from_args "$@")
|
||||
local peer=$(_get_peer6_from_args "$@")
|
||||
local port=$(_get_port_from_args "$@")
|
||||
local comment="phantun_${tun}_${port}"
|
||||
|
||||
if _check_rule6_by_comment "${comment}"; then
|
||||
warn "ip6tables rules already exist, maybe needs to check."
|
||||
else
|
||||
_ip6tables -A FORWARD -i $tun -j ACCEPT -m comment --comment "${comment}" || error "ip6tables filter rule add failed."
|
||||
_ip6tables -A FORWARD -o $tun -j ACCEPT -m comment --comment "${comment}" || error "ip6tables filter rule add failed."
|
||||
if _is_server_mode "$1"; then
|
||||
info "ip6tables DNAT rule added: [${comment}]: ${interface} -> ${tun}, ${address} -> ${peer}"
|
||||
_ip6tables -t nat -A PREROUTING -p tcp -i $interface --dport $port -j DNAT --to-destination $peer \
|
||||
-m comment --comment "${comment}" || error "ip6tables DNAT rule add failed."
|
||||
else
|
||||
info "ip6tables SNAT rule added: [${comment}]: ${tun} -> ${interface}, ${peer} -> ${address}"
|
||||
_ip6tables -t nat -A POSTROUTING -s $peer -o $interface -j SNAT --to-source $address \
|
||||
-m comment --comment "${comment}" || error "ip6tables SNAT rule add failed."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
stop_process() {
|
||||
kill $(pidof phantun-server phantun-client)
|
||||
info "terminate phantun process."
|
||||
}
|
||||
|
||||
revoke_iptables() {
|
||||
local tun=$(_get_tun_from_args "$@")
|
||||
local port=$(_get_port_from_args "$@")
|
||||
local comment="phantun_${tun}_${port}"
|
||||
|
||||
iptables-save -t filter | grep "${comment}" | while read rule; do
|
||||
_iptables -t filter ${rule/-A/-D} || error "iptables filter rule remove failed."
|
||||
done
|
||||
iptables-save -t nat | grep "${comment}" | while read rule; do
|
||||
_iptables -t nat ${rule/-A/-D} || error "iptables nat rule remove failed."
|
||||
done
|
||||
info "iptables rule: [${comment}] removed."
|
||||
}
|
||||
|
||||
revoke_ip6tables() {
|
||||
! _is_ipv4_only "$@" || return
|
||||
|
||||
local tun=$(_get_tun_from_args "$@")
|
||||
local port=$(_get_port_from_args "$@")
|
||||
local comment="phantun_${tun}_${port}"
|
||||
|
||||
ip6tables-save -t filter | grep "${comment}" | while read rule; do
|
||||
_ip6tables -t filter ${rule/-A/-D} || error "ip6tables filter rule remove failed."
|
||||
done
|
||||
ip6tables-save -t nat | grep "${comment}" | while read rule; do
|
||||
_ip6tables -t nat ${rule/-A/-D} || error "ip6tables nat rule remove failed."
|
||||
done
|
||||
info "ip6tables rule: [${comment}] removed."
|
||||
}
|
||||
|
||||
graceful_stop() {
|
||||
warn "caught SIGTERM or SIGINT signal, graceful stopping..."
|
||||
stop_process
|
||||
revoke_iptables "$@"
|
||||
revoke_ip6tables "$@"
|
||||
}
|
||||
|
||||
start_phantun() {
|
||||
trap 'graceful_stop "$@"' SIGTERM SIGINT
|
||||
apply_sysctl "$@"
|
||||
apply_iptables "$@"
|
||||
apply_ip6tables "$@"
|
||||
"$@" &
|
||||
wait
|
||||
}
|
||||
|
||||
start_phantun "$@"
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fake-tcp"
|
||||
version = "0.2.4"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
authors = ["Datong Sun <dndx@idndx.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -16,9 +16,10 @@ benchmark = []
|
||||
|
||||
[dependencies]
|
||||
bytes = "1"
|
||||
pnet = "0.29"
|
||||
tokio = { version = "1.14", features = ["full"] }
|
||||
rand = { version = "0.8", features = ["small_rng"] }
|
||||
log = "0.4"
|
||||
internet-checksum = "0.2"
|
||||
tokio-tun = "0.5"
|
||||
pnet = "0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
rand = { version = "0", features = ["small_rng"] }
|
||||
log = "0"
|
||||
internet-checksum = "0"
|
||||
tokio-tun = "0"
|
||||
flume = "0"
|
||||
|
@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
|
||||
Copyright 2021-2024 Datong Sun (dndx@idndx.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
|
||||
Copyright (c) 2021-2024 Datong Sun (dndx@idndx.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
@ -49,27 +49,30 @@ use pnet::packet::{tcp, Packet};
|
||||
use rand::prelude::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc, RwLock,
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time;
|
||||
use tokio_tun::Tun;
|
||||
|
||||
const TIMEOUT: time::Duration = time::Duration::from_secs(1);
|
||||
const RETRIES: usize = 6;
|
||||
const MPSC_BUFFER_LEN: usize = 512;
|
||||
const MPMC_BUFFER_LEN: usize = 512;
|
||||
const MPSC_BUFFER_LEN: usize = 128;
|
||||
const MAX_UNACKED_LEN: u32 = 128 * 1024 * 1024; // 128MB
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
|
||||
struct AddrTuple {
|
||||
local_addr: SocketAddrV4,
|
||||
remote_addr: SocketAddrV4,
|
||||
local_addr: SocketAddr,
|
||||
remote_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl AddrTuple {
|
||||
fn new(local_addr: SocketAddrV4, remote_addr: SocketAddrV4) -> AddrTuple {
|
||||
fn new(local_addr: SocketAddr, remote_addr: SocketAddr) -> AddrTuple {
|
||||
AddrTuple {
|
||||
local_addr,
|
||||
remote_addr,
|
||||
@ -78,17 +81,18 @@ impl AddrTuple {
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
tuples: RwLock<HashMap<AddrTuple, Sender<Bytes>>>,
|
||||
tuples: RwLock<HashMap<AddrTuple, flume::Sender<Bytes>>>,
|
||||
listening: RwLock<HashSet<u16>>,
|
||||
tun: Vec<Arc<Tun>>,
|
||||
ready: Sender<Socket>,
|
||||
ready: mpsc::Sender<Socket>,
|
||||
tuples_purge: broadcast::Sender<AddrTuple>,
|
||||
}
|
||||
|
||||
pub struct Stack {
|
||||
shared: Arc<Shared>,
|
||||
local_ip: Ipv4Addr,
|
||||
ready: Receiver<Socket>,
|
||||
local_ip6: Option<Ipv6Addr>,
|
||||
ready: mpsc::Receiver<Socket>,
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
@ -101,11 +105,12 @@ pub enum State {
|
||||
pub struct Socket {
|
||||
shared: Arc<Shared>,
|
||||
tun: Arc<Tun>,
|
||||
incoming: AsyncMutex<Receiver<Bytes>>,
|
||||
local_addr: SocketAddrV4,
|
||||
remote_addr: SocketAddrV4,
|
||||
incoming: flume::Receiver<Bytes>,
|
||||
local_addr: SocketAddr,
|
||||
remote_addr: SocketAddr,
|
||||
seq: AtomicU32,
|
||||
ack: AtomicU32,
|
||||
last_ack: AtomicU32,
|
||||
state: State,
|
||||
}
|
||||
|
||||
@ -120,34 +125,38 @@ impl Socket {
|
||||
fn new(
|
||||
shared: Arc<Shared>,
|
||||
tun: Arc<Tun>,
|
||||
local_addr: SocketAddrV4,
|
||||
remote_addr: SocketAddrV4,
|
||||
local_addr: SocketAddr,
|
||||
remote_addr: SocketAddr,
|
||||
ack: Option<u32>,
|
||||
state: State,
|
||||
) -> (Socket, Sender<Bytes>) {
|
||||
let (incoming_tx, incoming_rx) = mpsc::channel(MPSC_BUFFER_LEN);
|
||||
) -> (Socket, flume::Sender<Bytes>) {
|
||||
let (incoming_tx, incoming_rx) = flume::bounded(MPMC_BUFFER_LEN);
|
||||
|
||||
(
|
||||
Socket {
|
||||
shared,
|
||||
tun,
|
||||
incoming: AsyncMutex::new(incoming_rx),
|
||||
incoming: incoming_rx,
|
||||
local_addr,
|
||||
remote_addr,
|
||||
seq: AtomicU32::new(0),
|
||||
ack: AtomicU32::new(ack.unwrap_or(0)),
|
||||
last_ack: AtomicU32::new(ack.unwrap_or(0)),
|
||||
state,
|
||||
},
|
||||
incoming_tx,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_tcp_packet(&self, flags: u16, payload: Option<&[u8]>) -> Bytes {
|
||||
fn build_tcp_packet(&self, flags: u8, payload: Option<&[u8]>) -> Bytes {
|
||||
let ack = self.ack.load(Ordering::Relaxed);
|
||||
self.last_ack.store(ack, Ordering::Relaxed);
|
||||
|
||||
build_tcp_packet(
|
||||
self.local_addr,
|
||||
self.remote_addr,
|
||||
self.seq.load(Ordering::Relaxed),
|
||||
self.ack.load(Ordering::Relaxed),
|
||||
ack,
|
||||
flags,
|
||||
payload,
|
||||
)
|
||||
@ -165,12 +174,7 @@ impl Socket {
|
||||
State::Established => {
|
||||
let buf = self.build_tcp_packet(tcp::TcpFlags::ACK, Some(payload));
|
||||
self.seq.fetch_add(payload.len() as u32, Ordering::Relaxed);
|
||||
|
||||
tokio::select! {
|
||||
res = self.tun.send(&buf) => {
|
||||
res.ok().and(Some(()))
|
||||
},
|
||||
}
|
||||
self.tun.send(&buf).await.ok().and(Some(()))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@ -186,9 +190,8 @@ impl Socket {
|
||||
pub async fn recv(&self, buf: &mut [u8]) -> Option<usize> {
|
||||
match self.state {
|
||||
State::Established => {
|
||||
let mut incoming = self.incoming.lock().await;
|
||||
incoming.recv().await.and_then(|raw_buf| {
|
||||
let (_v4_packet, tcp_packet) = parse_ipv4_packet(&raw_buf);
|
||||
self.incoming.recv_async().await.ok().and_then(|raw_buf| {
|
||||
let (_v4_packet, tcp_packet) = parse_ip_packet(&raw_buf).unwrap();
|
||||
|
||||
if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 {
|
||||
info!("Connection {} reset by peer", self);
|
||||
@ -197,8 +200,18 @@ impl Socket {
|
||||
|
||||
let payload = tcp_packet.payload();
|
||||
|
||||
self.ack
|
||||
.store(tcp_packet.get_sequence().wrapping_add(1), Ordering::Relaxed);
|
||||
let new_ack = tcp_packet.get_sequence().wrapping_add(payload.len() as u32);
|
||||
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);
|
||||
|
||||
@ -220,10 +233,10 @@ impl Socket {
|
||||
info!("Sent SYN + ACK to client");
|
||||
}
|
||||
State::SynReceived => {
|
||||
let res = time::timeout(TIMEOUT, self.incoming.lock().await.recv()).await;
|
||||
let res = time::timeout(TIMEOUT, self.incoming.recv_async()).await;
|
||||
if let Ok(buf) = res {
|
||||
let buf = buf.unwrap();
|
||||
let (_v4_packet, tcp_packet) = parse_ipv4_packet(&buf);
|
||||
let (_v4_packet, tcp_packet) = parse_ip_packet(&buf).unwrap();
|
||||
|
||||
if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 {
|
||||
return;
|
||||
@ -264,10 +277,10 @@ impl Socket {
|
||||
info!("Sent SYN to server");
|
||||
}
|
||||
State::SynSent => {
|
||||
match time::timeout(TIMEOUT, self.incoming.lock().await.recv()).await {
|
||||
match time::timeout(TIMEOUT, self.incoming.recv_async()).await {
|
||||
Ok(buf) => {
|
||||
let buf = buf.unwrap();
|
||||
let (_v4_packet, tcp_packet) = parse_ipv4_packet(&buf);
|
||||
let (_v4_packet, tcp_packet) = parse_ip_packet(&buf).unwrap();
|
||||
|
||||
if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 {
|
||||
return None;
|
||||
@ -315,7 +328,14 @@ impl Drop for Socket {
|
||||
// purge cache
|
||||
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) {
|
||||
warn!("Unable to send RST to remote end: {}", e);
|
||||
}
|
||||
@ -341,7 +361,7 @@ impl Stack {
|
||||
/// 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>, local_ip: Ipv4Addr, local_ip6: Option<Ipv6Addr>) -> Stack {
|
||||
let tun: Vec<Arc<Tun>> = tun.into_iter().map(Arc::new).collect();
|
||||
let (ready_tx, ready_rx) = mpsc::channel(MPSC_BUFFER_LEN);
|
||||
let (tuples_purge_tx, _tuples_purge_rx) = broadcast::channel(16);
|
||||
@ -352,7 +372,6 @@ impl Stack {
|
||||
ready: ready_tx,
|
||||
tuples_purge: tuples_purge_tx.clone(),
|
||||
});
|
||||
let local_ip = tun[0].destination().unwrap();
|
||||
|
||||
for t in tun {
|
||||
tokio::spawn(Stack::reader_task(
|
||||
@ -365,6 +384,7 @@ impl Stack {
|
||||
Stack {
|
||||
shared,
|
||||
local_ip,
|
||||
local_ip6,
|
||||
ready: ready_rx,
|
||||
}
|
||||
}
|
||||
@ -381,26 +401,51 @@ impl Stack {
|
||||
|
||||
/// Connects to the remote end. `None` returned means
|
||||
/// the connection attempt failed.
|
||||
pub async fn connect(&mut self, addr: SocketAddrV4) -> Option<Socket> {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let local_port: u16 = rng.gen_range(1024..65535);
|
||||
let local_addr = SocketAddrV4::new(self.local_ip, local_port);
|
||||
let tuple = AddrTuple::new(local_addr, addr);
|
||||
let (mut sock, incoming) = Socket::new(
|
||||
self.shared.clone(),
|
||||
self.shared.tun.choose(&mut rng).unwrap().clone(),
|
||||
local_addr,
|
||||
addr,
|
||||
None,
|
||||
State::Idle,
|
||||
);
|
||||
pub async fn connect(&mut self, addr: SocketAddr) -> Option<Socket> {
|
||||
let mut rng = SmallRng::from_os_rng();
|
||||
for local_port in rng.random_range(32768..=60999)..=60999 {
|
||||
let local_addr = SocketAddr::new(
|
||||
if addr.is_ipv4() {
|
||||
IpAddr::V4(self.local_ip)
|
||||
} else {
|
||||
IpAddr::V6(self.local_ip6.expect("IPv6 local address undefined"))
|
||||
},
|
||||
local_port,
|
||||
);
|
||||
let tuple = AddrTuple::new(local_addr, addr);
|
||||
let mut sock;
|
||||
|
||||
{
|
||||
let mut tuples = self.shared.tuples.write().unwrap();
|
||||
assert!(tuples.insert(tuple, incoming.clone()).is_none());
|
||||
{
|
||||
let mut tuples = self.shared.tuples.write().unwrap();
|
||||
if tuples.contains_key(&tuple) {
|
||||
trace!(
|
||||
"Fake TCP connection to {}, local port number {} already in use, trying another one",
|
||||
addr, local_port
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let incoming;
|
||||
(sock, incoming) = Socket::new(
|
||||
self.shared.clone(),
|
||||
self.shared.tun.choose(&mut rng).unwrap().clone(),
|
||||
local_addr,
|
||||
addr,
|
||||
None,
|
||||
State::Idle,
|
||||
);
|
||||
|
||||
assert!(tuples.insert(tuple, incoming).is_none());
|
||||
}
|
||||
|
||||
return sock.connect().await.map(|_| sock);
|
||||
}
|
||||
|
||||
sock.connect().await.map(|_| sock)
|
||||
error!(
|
||||
"Fake TCP connection to {} failed, emphemeral port number exhausted",
|
||||
addr
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
async fn reader_task(
|
||||
@ -408,11 +453,10 @@ impl Stack {
|
||||
shared: Arc<Shared>,
|
||||
mut tuples_purge: broadcast::Receiver<AddrTuple>,
|
||||
) {
|
||||
let mut tuples: HashMap<AddrTuple, Sender<Bytes>> = HashMap::new();
|
||||
let mut tuples: HashMap<AddrTuple, flume::Sender<Bytes>> = HashMap::new();
|
||||
|
||||
loop {
|
||||
let mut buf = BytesMut::with_capacity(MAX_PACKET_LEN);
|
||||
buf.resize(MAX_PACKET_LEN, 0);
|
||||
let mut buf = BytesMut::zeroed(MAX_PACKET_LEN);
|
||||
|
||||
tokio::select! {
|
||||
size = tun.recv(&mut buf) => {
|
||||
@ -420,95 +464,95 @@ impl Stack {
|
||||
buf.truncate(size);
|
||||
let buf = buf.freeze();
|
||||
|
||||
if buf[0] >> 4 != 4 {
|
||||
// not an IPv4 packet
|
||||
continue;
|
||||
}
|
||||
match parse_ip_packet(&buf) {
|
||||
Some((ip_packet, tcp_packet)) => {
|
||||
let local_addr =
|
||||
SocketAddr::new(ip_packet.get_destination(), tcp_packet.get_destination());
|
||||
let remote_addr = SocketAddr::new(ip_packet.get_source(), tcp_packet.get_source());
|
||||
|
||||
let (ip_packet, tcp_packet) = parse_ipv4_packet(&buf);
|
||||
let local_addr =
|
||||
SocketAddrV4::new(ip_packet.get_destination(), tcp_packet.get_destination());
|
||||
let remote_addr = SocketAddrV4::new(ip_packet.get_source(), tcp_packet.get_source());
|
||||
let tuple = AddrTuple::new(local_addr, remote_addr);
|
||||
if let Some(c) = tuples.get(&tuple) {
|
||||
if c.send_async(buf).await.is_err() {
|
||||
trace!("Cache hit, but receiver already closed, dropping packet");
|
||||
}
|
||||
|
||||
let tuple = AddrTuple::new(local_addr, remote_addr);
|
||||
if let Some(c) = tuples.get(&tuple) {
|
||||
if c.send(buf).await.is_err() {
|
||||
trace!("Cache hit, but receiver already closed, dropping packet");
|
||||
continue;
|
||||
|
||||
// If not Ok, receiver has been closed and just fall through to the slow
|
||||
// path below
|
||||
} else {
|
||||
trace!("Cache miss, checking the shared tuples table for connection");
|
||||
let sender = {
|
||||
let tuples = shared.tuples.read().unwrap();
|
||||
tuples.get(&tuple).cloned()
|
||||
};
|
||||
|
||||
if let Some(c) = sender {
|
||||
trace!("Storing connection information into local tuples");
|
||||
tuples.insert(tuple, c.clone());
|
||||
c.send_async(buf).await.unwrap();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if tcp_packet.get_flags() == tcp::TcpFlags::SYN
|
||||
&& shared
|
||||
.listening
|
||||
.read()
|
||||
.unwrap()
|
||||
.contains(&tcp_packet.get_destination())
|
||||
{
|
||||
// SYN seen on listening socket
|
||||
if tcp_packet.get_sequence() == 0 {
|
||||
let (sock, incoming) = Socket::new(
|
||||
shared.clone(),
|
||||
tun.clone(),
|
||||
local_addr,
|
||||
remote_addr,
|
||||
Some(tcp_packet.get_sequence() + 1),
|
||||
State::Idle,
|
||||
);
|
||||
assert!(shared
|
||||
.tuples
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(tuple, incoming)
|
||||
.is_none());
|
||||
tokio::spawn(sock.accept());
|
||||
} else {
|
||||
trace!("Bad TCP SYN packet from {}, sending RST", remote_addr);
|
||||
let buf = build_tcp_packet(
|
||||
local_addr,
|
||||
remote_addr,
|
||||
0,
|
||||
tcp_packet.get_sequence() + tcp_packet.payload().len() as u32 + 1, // +1 because of SYN flag set
|
||||
tcp::TcpFlags::RST | tcp::TcpFlags::ACK,
|
||||
None,
|
||||
);
|
||||
shared.tun[0].try_send(&buf).unwrap();
|
||||
}
|
||||
} else if (tcp_packet.get_flags() & tcp::TcpFlags::RST) == 0 {
|
||||
info!("Unknown TCP packet from {}, sending RST", remote_addr);
|
||||
let buf = build_tcp_packet(
|
||||
local_addr,
|
||||
remote_addr,
|
||||
tcp_packet.get_acknowledgement(),
|
||||
tcp_packet.get_sequence() + tcp_packet.payload().len() as u32,
|
||||
tcp::TcpFlags::RST | tcp::TcpFlags::ACK,
|
||||
None,
|
||||
);
|
||||
shared.tun[0].try_send(&buf).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
|
||||
// If not Ok, receiver has been closed and just fall through to the slow
|
||||
// path below
|
||||
|
||||
} else {
|
||||
trace!("Cache miss, checking the shared tuples table for connection");
|
||||
let sender = {
|
||||
let tuples = shared.tuples.read().unwrap();
|
||||
tuples.get(&tuple).cloned()
|
||||
};
|
||||
|
||||
if let Some(c) = sender {
|
||||
trace!("Storing connection information into local tuples");
|
||||
tuples.insert(tuple, c.clone());
|
||||
c.send(buf).await.unwrap();
|
||||
None => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if tcp_packet.get_flags() == tcp::TcpFlags::SYN
|
||||
&& shared
|
||||
.listening
|
||||
.read()
|
||||
.unwrap()
|
||||
.contains(&tcp_packet.get_destination())
|
||||
{
|
||||
// SYN seen on listening socket
|
||||
if tcp_packet.get_sequence() == 0 {
|
||||
let (sock, incoming) = Socket::new(
|
||||
shared.clone(),
|
||||
tun.clone(),
|
||||
local_addr,
|
||||
remote_addr,
|
||||
Some(tcp_packet.get_sequence() + 1),
|
||||
State::Idle,
|
||||
);
|
||||
assert!(shared
|
||||
.tuples
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(tuple, incoming)
|
||||
.is_none());
|
||||
tokio::spawn(sock.accept());
|
||||
} else {
|
||||
trace!("Bad TCP SYN packet from {}, sending RST", remote_addr);
|
||||
let buf = build_tcp_packet(
|
||||
local_addr,
|
||||
remote_addr,
|
||||
0,
|
||||
tcp_packet.get_sequence() + 1,
|
||||
tcp::TcpFlags::RST,
|
||||
None,
|
||||
);
|
||||
shared.tun[0].try_send(&buf).unwrap();
|
||||
}
|
||||
} else if (tcp_packet.get_flags() & tcp::TcpFlags::RST) == 0 {
|
||||
info!("Unknown TCP packet from {}, sending RST", remote_addr);
|
||||
let buf = build_tcp_packet(
|
||||
local_addr,
|
||||
remote_addr,
|
||||
tcp_packet.get_acknowledgement(),
|
||||
0,
|
||||
tcp::TcpFlags::RST,
|
||||
None,
|
||||
);
|
||||
shared.tun[0].try_send(&buf).unwrap();
|
||||
}
|
||||
},
|
||||
tuple = tuples_purge.recv() => {
|
||||
let tuple = tuple.unwrap();
|
||||
tuples.remove(&tuple);
|
||||
trace!("Removed cached tuple");
|
||||
trace!("Removed cached tuple: {:?}", tuple);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,84 @@
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use internet_checksum::Checksum;
|
||||
use pnet::packet::Packet;
|
||||
use pnet::packet::{ip, ipv4, tcp};
|
||||
use pnet::packet::{ip, ipv4, ipv6, tcp};
|
||||
use std::convert::TryInto;
|
||||
use std::net::SocketAddrV4;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
const IPV4_HEADER_LEN: usize = 20;
|
||||
const IPV6_HEADER_LEN: usize = 40;
|
||||
const TCP_HEADER_LEN: usize = 20;
|
||||
pub const MAX_PACKET_LEN: usize = 1500;
|
||||
|
||||
pub enum IPPacket<'p> {
|
||||
V4(ipv4::Ipv4Packet<'p>),
|
||||
V6(ipv6::Ipv6Packet<'p>),
|
||||
}
|
||||
|
||||
impl IPPacket<'_> {
|
||||
pub fn get_source(&self) -> IpAddr {
|
||||
match self {
|
||||
IPPacket::V4(p) => IpAddr::V4(p.get_source()),
|
||||
IPPacket::V6(p) => IpAddr::V6(p.get_source()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_destination(&self) -> IpAddr {
|
||||
match self {
|
||||
IPPacket::V4(p) => IpAddr::V4(p.get_destination()),
|
||||
IPPacket::V6(p) => IpAddr::V6(p.get_destination()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_tcp_packet(
|
||||
local_addr: SocketAddrV4,
|
||||
remote_addr: SocketAddrV4,
|
||||
local_addr: SocketAddr,
|
||||
remote_addr: SocketAddr,
|
||||
seq: u32,
|
||||
ack: u32,
|
||||
flags: u16,
|
||||
flags: u8,
|
||||
payload: Option<&[u8]>,
|
||||
) -> Bytes {
|
||||
let ip_header_len = match local_addr {
|
||||
SocketAddr::V4(_) => IPV4_HEADER_LEN,
|
||||
SocketAddr::V6(_) => IPV6_HEADER_LEN,
|
||||
};
|
||||
let wscale = (flags & tcp::TcpFlags::SYN) != 0;
|
||||
let tcp_header_len = TCP_HEADER_LEN + if wscale { 4 } else { 0 }; // nop + wscale
|
||||
let tcp_total_len = tcp_header_len + payload.map_or(0, |payload| payload.len());
|
||||
let total_len = IPV4_HEADER_LEN + tcp_total_len;
|
||||
let mut buf = BytesMut::with_capacity(total_len);
|
||||
buf.resize(total_len, 0);
|
||||
let total_len = ip_header_len + tcp_total_len;
|
||||
let mut buf = BytesMut::zeroed(total_len);
|
||||
|
||||
let mut v4_buf = buf.split_to(IPV4_HEADER_LEN);
|
||||
let mut ip_buf = buf.split_to(ip_header_len);
|
||||
let mut tcp_buf = buf.split_to(tcp_total_len);
|
||||
assert_eq!(0, buf.len());
|
||||
|
||||
let mut v4 = ipv4::MutableIpv4Packet::new(&mut v4_buf).unwrap();
|
||||
v4.set_version(4);
|
||||
v4.set_header_length(IPV4_HEADER_LEN as u8 / 4);
|
||||
v4.set_next_level_protocol(ip::IpNextHeaderProtocols::Tcp);
|
||||
v4.set_ttl(64);
|
||||
v4.set_source(*local_addr.ip());
|
||||
v4.set_destination(*remote_addr.ip());
|
||||
v4.set_total_length(total_len.try_into().unwrap());
|
||||
v4.set_flags(ipv4::Ipv4Flags::DontFragment);
|
||||
let mut cksm = Checksum::new();
|
||||
cksm.add_bytes(v4.packet());
|
||||
v4.set_checksum(u16::from_be_bytes(cksm.checksum()));
|
||||
match (local_addr, remote_addr) {
|
||||
(SocketAddr::V4(local), SocketAddr::V4(remote)) => {
|
||||
let mut v4 = ipv4::MutableIpv4Packet::new(&mut ip_buf).unwrap();
|
||||
v4.set_version(4);
|
||||
v4.set_header_length(IPV4_HEADER_LEN as u8 / 4);
|
||||
v4.set_next_level_protocol(ip::IpNextHeaderProtocols::Tcp);
|
||||
v4.set_ttl(64);
|
||||
v4.set_source(*local.ip());
|
||||
v4.set_destination(*remote.ip());
|
||||
v4.set_total_length(total_len.try_into().unwrap());
|
||||
v4.set_flags(ipv4::Ipv4Flags::DontFragment);
|
||||
let mut cksm = Checksum::new();
|
||||
cksm.add_bytes(v4.packet());
|
||||
v4.set_checksum(u16::from_be_bytes(cksm.checksum()));
|
||||
}
|
||||
(SocketAddr::V6(local), SocketAddr::V6(remote)) => {
|
||||
let mut v6 = ipv6::MutableIpv6Packet::new(&mut ip_buf).unwrap();
|
||||
v6.set_version(6);
|
||||
v6.set_payload_length(tcp_total_len.try_into().unwrap());
|
||||
v6.set_next_header(ip::IpNextHeaderProtocols::Tcp);
|
||||
v6.set_hop_limit(64);
|
||||
v6.set_source(*local.ip());
|
||||
v6.set_destination(*remote.ip());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut tcp = tcp::MutableTcpPacket::new(&mut tcp_buf).unwrap();
|
||||
tcp.set_window(0xffff);
|
||||
@ -59,24 +98,55 @@ pub fn build_tcp_packet(
|
||||
}
|
||||
|
||||
let mut cksm = Checksum::new();
|
||||
cksm.add_bytes(&local_addr.ip().octets());
|
||||
cksm.add_bytes(&remote_addr.ip().octets());
|
||||
let ip::IpNextHeaderProtocol(tcp_protocol) = ip::IpNextHeaderProtocols::Tcp;
|
||||
let mut pseudo = [0u8, tcp_protocol, 0, 0];
|
||||
pseudo[2..].copy_from_slice(&(tcp_total_len as u16).to_be_bytes());
|
||||
cksm.add_bytes(&pseudo);
|
||||
|
||||
match (local_addr, remote_addr) {
|
||||
(SocketAddr::V4(local), SocketAddr::V4(remote)) => {
|
||||
cksm.add_bytes(&local.ip().octets());
|
||||
cksm.add_bytes(&remote.ip().octets());
|
||||
|
||||
let mut pseudo = [0u8, tcp_protocol, 0, 0];
|
||||
pseudo[2..].copy_from_slice(&(tcp_total_len as u16).to_be_bytes());
|
||||
cksm.add_bytes(&pseudo);
|
||||
}
|
||||
(SocketAddr::V6(local), SocketAddr::V6(remote)) => {
|
||||
cksm.add_bytes(&local.ip().octets());
|
||||
cksm.add_bytes(&remote.ip().octets());
|
||||
|
||||
let mut pseudo = [0u8, 0, 0, 0, 0, 0, 0, tcp_protocol];
|
||||
pseudo[0..4].copy_from_slice(&(tcp_total_len as u32).to_be_bytes());
|
||||
cksm.add_bytes(&pseudo);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cksm.add_bytes(tcp.packet());
|
||||
tcp.set_checksum(u16::from_be_bytes(cksm.checksum()));
|
||||
|
||||
v4_buf.unsplit(tcp_buf);
|
||||
v4_buf.freeze()
|
||||
ip_buf.unsplit(tcp_buf);
|
||||
ip_buf.freeze()
|
||||
}
|
||||
|
||||
pub fn parse_ipv4_packet(buf: &Bytes) -> (ipv4::Ipv4Packet, tcp::TcpPacket) {
|
||||
let v4 = ipv4::Ipv4Packet::new(buf).unwrap();
|
||||
let tcp = tcp::TcpPacket::new(&buf[IPV4_HEADER_LEN..]).unwrap();
|
||||
pub fn parse_ip_packet(buf: &Bytes) -> Option<(IPPacket, tcp::TcpPacket)> {
|
||||
if buf[0] >> 4 == 4 {
|
||||
let v4 = ipv4::Ipv4Packet::new(buf).unwrap();
|
||||
if v4.get_next_level_protocol() != ip::IpNextHeaderProtocols::Tcp {
|
||||
return None;
|
||||
}
|
||||
|
||||
(v4, tcp)
|
||||
let tcp = tcp::TcpPacket::new(&buf[IPV4_HEADER_LEN..]).unwrap();
|
||||
Some((IPPacket::V4(v4), tcp))
|
||||
} else if buf[0] >> 4 == 6 {
|
||||
let v6 = ipv6::Ipv6Packet::new(buf).unwrap();
|
||||
if v6.get_next_header() != ip::IpNextHeaderProtocols::Tcp {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tcp = tcp::TcpPacket::new(&buf[IPV6_HEADER_LEN..]).unwrap();
|
||||
Some((IPPacket::V6(v6), tcp))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "benchmark"))]
|
||||
|
BIN
images/packet-headers.png
Normal file
BIN
images/packet-headers.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
images/phantun-vs-udp2raw-benchmark-result.png
Normal file
BIN
images/phantun-vs-udp2raw-benchmark-result.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "phantun"
|
||||
version = "0.3.0"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
authors = ["Datong Sun <dndx@idndx.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -11,12 +11,14 @@ Transforms UDP stream into (fake) TCP streams that can go through
|
||||
Layer 3 & Layer 4 (NAPT) firewalls/NATs.
|
||||
"""
|
||||
[dependencies]
|
||||
clap = { version = "3.0", features = ["cargo"] }
|
||||
socket2 = { version = "0.4", features = ["all"] }
|
||||
fake-tcp = { path = "../fake-tcp", version = "0.2" }
|
||||
tokio = { version = "1.14", features = ["full"] }
|
||||
tokio-util = "0.7"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
tokio-tun = "0.5"
|
||||
num_cpus = "1.13"
|
||||
clap = { version = "4", features = ["cargo"] }
|
||||
socket2 = { version = "0", features = ["all"] }
|
||||
fake-tcp = { path = "../fake-tcp", version = "0" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = "0"
|
||||
log = "0"
|
||||
pretty_env_logger = "0"
|
||||
tokio-tun = "0"
|
||||
num_cpus = "1"
|
||||
neli = "0"
|
||||
nix = { version = "0", features = ["net"] }
|
||||
|
@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
|
||||
Copyright 2021-2024 Datong Sun (dndx@idndx.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
|
||||
Copyright (c) 2021-2024 Datong Sun (dndx@idndx.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
@ -1,9 +1,11 @@
|
||||
use clap::{crate_version, Arg, Command};
|
||||
use clap::{crate_version, Arg, ArgAction, Command};
|
||||
use fake_tcp::packet::MAX_PACKET_LEN;
|
||||
use fake_tcp::{Socket, Stack};
|
||||
use log::{debug, error, info};
|
||||
use phantun::utils::new_udp_reuseport;
|
||||
use phantun::utils::{assign_ipv6_address, new_udp_reuseport};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Notify, RwLock};
|
||||
@ -14,7 +16,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use phantun::UDP_TTL;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> io::Result<()> {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let matches = Command::new("Phantun Client")
|
||||
@ -27,7 +29,6 @@ async fn main() {
|
||||
.required(true)
|
||||
.value_name("IP:PORT")
|
||||
.help("Sets the IP and port where Phantun Client listens for incoming UDP datagrams, IPv6 address need to be specified as: \"[IPv6]:PORT\"")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("remote")
|
||||
@ -35,8 +36,7 @@ async fn main() {
|
||||
.long("remote")
|
||||
.required(true)
|
||||
.value_name("IP or HOST NAME:PORT")
|
||||
.help("Sets the address or host name and port where Phantun Client connects to Phantun Server")
|
||||
.takes_value(true),
|
||||
.help("Sets the address or host name and port where Phantun Client connects to Phantun Server, IPv6 address need to be specified as: \"[IPv6]:PORT\"")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun")
|
||||
@ -45,195 +45,258 @@ async fn main() {
|
||||
.value_name("tunX")
|
||||
.help("Sets the Tun interface name, if absent, pick the next available name")
|
||||
.default_value("")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun_local")
|
||||
.long("tun-local")
|
||||
.required(false)
|
||||
.value_name("IP")
|
||||
.help("Sets the Tun interface local address (O/S's end)")
|
||||
.help("Sets the Tun interface IPv4 local address (O/S's end)")
|
||||
.default_value("192.168.200.1")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun_peer")
|
||||
.long("tun-peer")
|
||||
.required(false)
|
||||
.value_name("IP")
|
||||
.help("Sets the Tun interface destination (peer) address (Phantun Client's end). \
|
||||
.help("Sets the Tun interface IPv4 destination (peer) address (Phantun Client's end). \
|
||||
You will need to setup SNAT/MASQUERADE rules on your Internet facing interface \
|
||||
in order for Phantun Client to connect to Phantun Server")
|
||||
.default_value("192.168.200.2")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ipv4_only")
|
||||
.long("ipv4-only")
|
||||
.short('4')
|
||||
.required(false)
|
||||
.help("Only use IPv4 address when connecting to remote")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with_all(["tun_local6", "tun_peer6"]),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun_local6")
|
||||
.long("tun-local6")
|
||||
.required(false)
|
||||
.value_name("IP")
|
||||
.help("Sets the Tun interface IPv6 local address (O/S's end)")
|
||||
.default_value("fcc8::1")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun_peer6")
|
||||
.long("tun-peer6")
|
||||
.required(false)
|
||||
.value_name("IP")
|
||||
.help("Sets the Tun interface IPv6 destination (peer) address (Phantun Client's end). \
|
||||
You will need to setup SNAT/MASQUERADE rules on your Internet facing interface \
|
||||
in order for Phantun Client to connect to Phantun Server")
|
||||
.default_value("fcc8::2")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("handshake_packet")
|
||||
.long("handshake-packet")
|
||||
.required(false)
|
||||
.value_name("PATH")
|
||||
.help("Specify a file, which, after TCP handshake, its content will be sent as the \
|
||||
first data packet to the server.\n\
|
||||
Note: ensure this file's size does not exceed the MTU of the outgoing interface. \
|
||||
The content is always sent out in a single packet and will not be further segmented")
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let local_addr: SocketAddr = matches
|
||||
.value_of("local")
|
||||
.get_one::<String>("local")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("bad local address");
|
||||
|
||||
let remote_addr = tokio::net::lookup_host(matches.value_of("remote").unwrap())
|
||||
let ipv4_only = matches.get_flag("ipv4_only");
|
||||
|
||||
let remote_addr = tokio::net::lookup_host(matches.get_one::<String>("remote").unwrap())
|
||||
.await
|
||||
.expect("bad remote address or host")
|
||||
.find(|addr| addr.is_ipv4())
|
||||
.expect("unable to resolve remote host name or no valid A record was returned");
|
||||
let remote_addr = if let SocketAddr::V4(addr) = remote_addr {
|
||||
addr
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
.find(|addr| !ipv4_only || addr.is_ipv4())
|
||||
.expect("unable to resolve remote host name");
|
||||
info!("Remote address is: {}", remote_addr);
|
||||
|
||||
let tun_local: Ipv4Addr = matches
|
||||
.value_of("tun_local")
|
||||
.get_one::<String>("tun_local")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("bad local address for Tun interface");
|
||||
let tun_peer: Ipv4Addr = matches
|
||||
.value_of("tun_peer")
|
||||
.get_one::<String>("tun_peer")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("bad peer address for Tun interface");
|
||||
|
||||
let (tun_local6, tun_peer6) = if matches.get_flag("ipv4_only") {
|
||||
(None, None)
|
||||
} else {
|
||||
(
|
||||
matches
|
||||
.get_one::<String>("tun_local6")
|
||||
.map(|v| v.parse().expect("bad local address for Tun interface")),
|
||||
matches
|
||||
.get_one::<String>("tun_peer6")
|
||||
.map(|v| v.parse().expect("bad peer address for Tun interface")),
|
||||
)
|
||||
};
|
||||
|
||||
let tun_name = matches.get_one::<String>("tun").unwrap();
|
||||
let handshake_packet: Option<Vec<u8>> = matches
|
||||
.get_one::<String>("handshake_packet")
|
||||
.map(fs::read)
|
||||
.transpose()?;
|
||||
|
||||
let num_cpus = num_cpus::get();
|
||||
info!("{} cores available", num_cpus);
|
||||
|
||||
let tun = TunBuilder::new()
|
||||
.name(matches.value_of("tun").unwrap()) // if name is empty, then it is set by kernel.
|
||||
.tap(false) // false (default): TUN, true: TAP.
|
||||
.packet_info(false) // false: IFF_NO_PI, default is true.
|
||||
.name(tun_name) // if name is empty, then it is set by kernel.
|
||||
.up() // or set it up manually using `sudo ip link set <tun-name> up`.
|
||||
.address(tun_local)
|
||||
.destination(tun_peer)
|
||||
.try_build_mq(num_cpus)
|
||||
.queues(num_cpus)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
if remote_addr.is_ipv6() {
|
||||
assign_ipv6_address(tun[0].name(), tun_local6.unwrap(), tun_peer6.unwrap());
|
||||
}
|
||||
|
||||
info!("Created TUN device {}", tun[0].name());
|
||||
|
||||
let udp_sock = Arc::new(new_udp_reuseport(local_addr));
|
||||
let connections = Arc::new(RwLock::new(HashMap::<SocketAddr, Arc<Socket>>::new()));
|
||||
|
||||
let mut stack = Stack::new(tun);
|
||||
let mut stack = Stack::new(tun, tun_peer, tun_peer6);
|
||||
|
||||
let main_loop = tokio::spawn(async move {
|
||||
let mut buf_r = [0u8; MAX_PACKET_LEN];
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
Ok((size, addr)) = udp_sock.recv_from(&mut buf_r) => {
|
||||
// seen UDP packet to listening socket, this means:
|
||||
// 1. It is a new UDP connection, or
|
||||
// 2. It is some extra packets not filtered by more specific
|
||||
// connected UDP socket yet
|
||||
if let Some(sock) = connections.read().await.get(&addr) {
|
||||
sock.send(&buf_r[..size]).await;
|
||||
continue;
|
||||
}
|
||||
let (size, addr) = udp_sock.recv_from(&mut buf_r).await?;
|
||||
// seen UDP packet to listening socket, this means:
|
||||
// 1. It is a new UDP connection, or
|
||||
// 2. It is some extra packets not filtered by more specific
|
||||
// connected UDP socket yet
|
||||
if let Some(sock) = connections.read().await.get(&addr) {
|
||||
sock.send(&buf_r[..size]).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("New UDP client from {}", addr);
|
||||
let sock = stack.connect(remote_addr).await;
|
||||
if sock.is_none() {
|
||||
error!("Unable to connect to remote {}", remote_addr);
|
||||
continue;
|
||||
}
|
||||
info!("New UDP client from {}", addr);
|
||||
let sock = stack.connect(remote_addr).await;
|
||||
if sock.is_none() {
|
||||
error!("Unable to connect to remote {}", remote_addr);
|
||||
continue;
|
||||
}
|
||||
|
||||
let sock = Arc::new(sock.unwrap());
|
||||
// send first packet
|
||||
let res = sock.send(&buf_r[..size]).await;
|
||||
if res.is_none() {
|
||||
continue;
|
||||
}
|
||||
let sock = Arc::new(sock.unwrap());
|
||||
if let Some(ref p) = handshake_packet {
|
||||
if sock.send(p).await.is_none() {
|
||||
error!("Failed to send handshake packet to remote, closing connection.");
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(connections.write().await.insert(addr, sock.clone()).is_none());
|
||||
debug!("inserted fake TCP socket into connection table");
|
||||
debug!("Sent handshake packet to: {}", sock);
|
||||
}
|
||||
|
||||
// spawn "fastpath" UDP socket and task, this will offload main task
|
||||
// from forwarding UDP packets
|
||||
// send first packet
|
||||
if sock.send(&buf_r[..size]).await.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let packet_received = Arc::new(Notify::new());
|
||||
let quit = CancellationToken::new();
|
||||
assert!(connections
|
||||
.write()
|
||||
.await
|
||||
.insert(addr, sock.clone())
|
||||
.is_none());
|
||||
debug!("inserted fake TCP socket into connection table");
|
||||
|
||||
for i in 0..num_cpus {
|
||||
let sock = sock.clone();
|
||||
let quit = quit.child_token();
|
||||
let packet_received = packet_received.clone();
|
||||
// spawn "fastpath" UDP socket and task, this will offload main task
|
||||
// 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();
|
||||
|
||||
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;
|
||||
}
|
||||
for i in 0..num_cpus {
|
||||
let sock = sock.clone();
|
||||
let quit = quit.clone();
|
||||
let packet_received = packet_received.clone();
|
||||
|
||||
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;
|
||||
},
|
||||
}
|
||||
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();
|
||||
|
||||
packet_received.notify_one();
|
||||
},
|
||||
_ = quit.cancelled() => {
|
||||
debug!("worker {} terminated", i);
|
||||
return;
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let connections = connections.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let read_timeout = time::sleep(UDP_TTL);
|
||||
let packet_received_fut = packet_received.notified();
|
||||
|
||||
tokio::select! {
|
||||
_ = read_timeout => {
|
||||
info!("No traffic seen in the last {:?}, closing connection", UDP_TTL);
|
||||
connections.write().await.remove(&addr);
|
||||
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;
|
||||
},
|
||||
_ = quit.cancelled() => {
|
||||
connections.write().await.remove(&addr);
|
||||
debug!("removed fake TCP socket from connections table");
|
||||
return;
|
||||
},
|
||||
_ = packet_received_fut => {},
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
let read_timeout = time::sleep(UDP_TTL);
|
||||
let packet_received_fut = packet_received.notified();
|
||||
|
||||
tokio::select! {
|
||||
_ = read_timeout => {
|
||||
info!("No traffic seen in the last {:?}, closing connection", UDP_TTL);
|
||||
connections.write().await.remove(&addr);
|
||||
debug!("removed fake TCP socket from connections table");
|
||||
|
||||
quit.cancel();
|
||||
return;
|
||||
},
|
||||
_ = quit.cancelled() => {
|
||||
connections.write().await.remove(&addr);
|
||||
debug!("removed fake TCP socket from connections table");
|
||||
return;
|
||||
},
|
||||
_ = packet_received_fut => {},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tokio::join!(main_loop).0.unwrap();
|
||||
tokio::join!(main_loop).0.unwrap()
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
use clap::{crate_version, Arg, Command};
|
||||
use clap::{crate_version, Arg, ArgAction, Command};
|
||||
use fake_tcp::packet::MAX_PACKET_LEN;
|
||||
use fake_tcp::Stack;
|
||||
use log::{debug, error, info};
|
||||
use phantun::utils::new_udp_reuseport;
|
||||
use phantun::utils::{assign_ipv6_address, new_udp_reuseport};
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::UdpSocket;
|
||||
@ -14,7 +16,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use phantun::UDP_TTL;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> io::Result<()> {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let matches = Command::new("Phantun Server")
|
||||
@ -27,7 +29,6 @@ async fn main() {
|
||||
.required(true)
|
||||
.value_name("PORT")
|
||||
.help("Sets the port where Phantun Server listens for incoming Phantun Client TCP connections")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("remote")
|
||||
@ -36,7 +37,6 @@ async fn main() {
|
||||
.required(true)
|
||||
.value_name("IP or HOST NAME:PORT")
|
||||
.help("Sets the address or host name and port where Phantun Server forwards UDP packets to, IPv6 address need to be specified as: \"[IPv6]:PORT\"")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun")
|
||||
@ -45,7 +45,6 @@ async fn main() {
|
||||
.value_name("tunX")
|
||||
.help("Sets the Tun interface name, if absent, pick the next available name")
|
||||
.default_value("")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun_local")
|
||||
@ -54,7 +53,6 @@ async fn main() {
|
||||
.value_name("IP")
|
||||
.help("Sets the Tun interface local address (O/S's end)")
|
||||
.default_value("192.168.201.1")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun_peer")
|
||||
@ -65,50 +63,110 @@ async fn main() {
|
||||
You will need to setup DNAT rules to this address in order for Phantun Server \
|
||||
to accept TCP traffic from Phantun Client")
|
||||
.default_value("192.168.201.2")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ipv4_only")
|
||||
.long("ipv4-only")
|
||||
.short('4')
|
||||
.required(false)
|
||||
.help("Do not assign IPv6 addresses to Tun interface")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with_all(["tun_local6", "tun_peer6"]),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun_local6")
|
||||
.long("tun-local6")
|
||||
.required(false)
|
||||
.value_name("IP")
|
||||
.help("Sets the Tun interface IPv6 local address (O/S's end)")
|
||||
.default_value("fcc9::1")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tun_peer6")
|
||||
.long("tun-peer6")
|
||||
.required(false)
|
||||
.value_name("IP")
|
||||
.help("Sets the Tun interface IPv6 destination (peer) address (Phantun Client's end). \
|
||||
You will need to setup SNAT/MASQUERADE rules on your Internet facing interface \
|
||||
in order for Phantun Client to connect to Phantun Server")
|
||||
.default_value("fcc9::2")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("handshake_packet")
|
||||
.long("handshake-packet")
|
||||
.required(false)
|
||||
.value_name("PATH")
|
||||
.help("Specify a file, which, after TCP handshake, its content will be sent as the \
|
||||
first data packet to the client.\n\
|
||||
Note: ensure this file's size does not exceed the MTU of the outgoing interface. \
|
||||
The content is always sent out in a single packet and will not be further segmented")
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let local_port: u16 = matches
|
||||
.value_of("local")
|
||||
.get_one::<String>("local")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("bad local port");
|
||||
|
||||
let remote_addr = tokio::net::lookup_host(matches.value_of("remote").unwrap())
|
||||
let remote_addr = tokio::net::lookup_host(matches.get_one::<String>("remote").unwrap())
|
||||
.await
|
||||
.expect("bad remote address or host")
|
||||
.next()
|
||||
.expect("unable to resolve remote host name");
|
||||
|
||||
info!("Remote address is: {}", remote_addr);
|
||||
|
||||
let tun_local: Ipv4Addr = matches
|
||||
.value_of("tun_local")
|
||||
.get_one::<String>("tun_local")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("bad local address for Tun interface");
|
||||
let tun_peer: Ipv4Addr = matches
|
||||
.value_of("tun_peer")
|
||||
.get_one::<String>("tun_peer")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("bad peer address for Tun interface");
|
||||
|
||||
let (tun_local6, tun_peer6) = if matches.get_flag("ipv4_only") {
|
||||
(None, None)
|
||||
} else {
|
||||
(
|
||||
matches
|
||||
.get_one::<String>("tun_local6")
|
||||
.map(|v| v.parse().expect("bad local address for Tun interface")),
|
||||
matches
|
||||
.get_one::<String>("tun_peer6")
|
||||
.map(|v| v.parse().expect("bad peer address for Tun interface")),
|
||||
)
|
||||
};
|
||||
|
||||
let tun_name = matches.get_one::<String>("tun").unwrap();
|
||||
let handshake_packet: Option<Vec<u8>> = matches
|
||||
.get_one::<String>("handshake_packet")
|
||||
.map(fs::read)
|
||||
.transpose()?;
|
||||
|
||||
let num_cpus = num_cpus::get();
|
||||
info!("{} cores available", num_cpus);
|
||||
|
||||
let tun = TunBuilder::new()
|
||||
.name(matches.value_of("tun").unwrap()) // if name is empty, then it is set by kernel.
|
||||
.tap(false) // false (default): TUN, true: TAP.
|
||||
.packet_info(false) // false: IFF_NO_PI, default is true.
|
||||
.name(tun_name) // if name is empty, then it is set by kernel.
|
||||
.up() // or set it up manually using `sudo ip link set <tun-name> up`.
|
||||
.address(tun_local)
|
||||
.destination(tun_peer)
|
||||
.try_build_mq(num_cpus)
|
||||
.queues(num_cpus)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
if let (Some(tun_local6), Some(tun_peer6)) = (tun_local6, tun_peer6) {
|
||||
assign_ipv6_address(tun[0].name(), tun_local6, tun_peer6);
|
||||
}
|
||||
|
||||
info!("Created TUN device {}", tun[0].name());
|
||||
|
||||
//thread::sleep(time::Duration::from_secs(5));
|
||||
let mut stack = Stack::new(tun);
|
||||
let mut stack = Stack::new(tun, tun_local, tun_local6);
|
||||
stack.listen(local_port);
|
||||
info!("Listening on {}", local_port);
|
||||
|
||||
@ -119,6 +177,14 @@ async fn main() {
|
||||
loop {
|
||||
let sock = Arc::new(stack.accept().await);
|
||||
info!("New connection: {}", sock);
|
||||
if let Some(ref p) = handshake_packet {
|
||||
if sock.send(p).await.is_none() {
|
||||
error!("Failed to send handshake packet to remote, closing connection.");
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Sent handshake packet to: {}", sock);
|
||||
}
|
||||
|
||||
let packet_received = Arc::new(Notify::new());
|
||||
let quit = CancellationToken::new();
|
||||
@ -127,14 +193,13 @@ async fn main() {
|
||||
} else {
|
||||
"[::]:0"
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let local_addr = udp_sock.local_addr().unwrap();
|
||||
.await?;
|
||||
let local_addr = udp_sock.local_addr()?;
|
||||
drop(udp_sock);
|
||||
|
||||
for i in 0..num_cpus {
|
||||
let sock = sock.clone();
|
||||
let quit = quit.child_token();
|
||||
let quit = quit.clone();
|
||||
let packet_received = packet_received.clone();
|
||||
let udp_sock = new_udp_reuseport(local_addr);
|
||||
|
||||
@ -198,5 +263,5 @@ async fn main() {
|
||||
}
|
||||
});
|
||||
|
||||
tokio::join!(main_loop).0.unwrap();
|
||||
tokio::join!(main_loop).0.unwrap()
|
||||
}
|
||||
|
@ -1,4 +1,15 @@
|
||||
use std::net::SocketAddr;
|
||||
use neli::{
|
||||
consts::{
|
||||
nl::{NlmF, NlmFFlags},
|
||||
rtnl::{Ifa, IfaFFlags, RtAddrFamily, Rtm},
|
||||
socket::NlFamily,
|
||||
},
|
||||
nl::{NlPayload, Nlmsghdr},
|
||||
rtnl::{Ifaddrmsg, Rtattr},
|
||||
socket::NlSocketHandle,
|
||||
types::RtBuffer,
|
||||
};
|
||||
use std::net::{Ipv6Addr, SocketAddr};
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
pub fn new_udp_reuseport(local_addr: SocketAddr) -> UdpSocket {
|
||||
@ -20,3 +31,30 @@ pub fn new_udp_reuseport(local_addr: SocketAddr) -> UdpSocket {
|
||||
let udp_sock: std::net::UdpSocket = udp_sock.into();
|
||||
udp_sock.try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn assign_ipv6_address(device_name: &str, local: Ipv6Addr, peer: Ipv6Addr) {
|
||||
let index = nix::net::if_::if_nametoindex(device_name).unwrap();
|
||||
|
||||
let mut rtnl = NlSocketHandle::connect(NlFamily::Route, None, &[]).unwrap();
|
||||
let mut rtattrs = RtBuffer::new();
|
||||
rtattrs.push(Rtattr::new(None, Ifa::Local, &local.octets()[..]).unwrap());
|
||||
rtattrs.push(Rtattr::new(None, Ifa::Address, &peer.octets()[..]).unwrap());
|
||||
|
||||
let ifaddrmsg = Ifaddrmsg {
|
||||
ifa_family: RtAddrFamily::Inet6,
|
||||
ifa_prefixlen: 128,
|
||||
ifa_flags: IfaFFlags::empty(),
|
||||
ifa_scope: 0,
|
||||
ifa_index: index as i32,
|
||||
rtattrs,
|
||||
};
|
||||
let nl_header = Nlmsghdr::new(
|
||||
None,
|
||||
Rtm::Newaddr,
|
||||
NlmFFlags::new(&[NlmF::Request]),
|
||||
None,
|
||||
None,
|
||||
NlPayload::Payload(ifaddrmsg),
|
||||
);
|
||||
rtnl.send(nl_header).unwrap();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user