GNU General Public License v2.0

项目介绍

bjin/phantun-dkms 是一个托管在 GitHub 上的开源项目,主要使用 Python 编写,采用 GNU General Public License v2.0 许可证,目前获得 10 个 Star、2 次 Fork。

仓库全名
bjin/phantun-dkms
开源许可证
GNU General Public License v2.0
Star 数
10
Fork 数
2
未关闭 Issue
1

README 摘要

phantun-dkms If you already know Phantun: this is a Linux kernel module implementation of Phantun's fake-TCP idea. Phantun runs as a user-space client/server pair around a TUN interface. phantun-dkms keeps the translation in the kernel, intercepts existing UDP sockets directly, and avoids the TUN topology. Phantun compatibility phantun-dkms is not advertised as compatible with Phantun endpoints. The important…

完整内容见 GitHub 原始 README 或本页 。

常见问题

phantun-dkms 用什么语言开发?

主要使用 Python 编写,采用 GNU General Public License v2.0 许可证,目前有 10 个 Star。

phantun-dkms 可以免费商用吗?

仓库标注的许可证为 GNU General Public License v2.0,商用前请先阅读仓库 LICENSE 文件确认授权范围。

如何获取 phantun-dkms 的源码?

可以直接克隆仓库:git clone https://github.com/bjin/phantun-dkms.git,默认分支为 master;安装与运行步骤见仓库 README。

phantun-dkms 最近还在维护吗?

仓库最近一次更新为 2 周前,当前有 1 个未关闭 Issue;Star 数为 10,可作为社区活跃度的参考。

关于

GitHub 原始描述(英文)

Kernel module re-implementation of phantun, transform UDP streams into fake-TCP streams

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

简要分析

热度分
14
价值分
6
活跃状态
活跃

语言占比

C
M4
Makefile
Nix
Python
Shell

Release

贡献者

README

phantun-dkms

Latest Release GitHub branch status

If you already know Phantun: this is a Linux kernel module implementation of Phantun’s fake-TCP idea.

Phantun runs as a user-space client/server pair around a TUN interface. phantun-dkms keeps the translation in the kernel, intercepts existing UDP sockets directly, and avoids the TUN topology.

Phantun compatibility

phantun-dkms is not advertised as compatible with Phantun endpoints.

The important nuance: the basic wire packet shape is intentionally the same, so the TCP/UDP header overhead story is the same too. For MTU budgeting, Phantun’s documentation is still the right mental model.

What changed is the behavioral contract around that wire format. I have not tried to make mixed phantun / phantun-dkms endpoints interoperate, and I would currently assume they most likely will not work seamlessly.

Main reasons:

  • Untested interoperability: mixed deployments have not been validated.
  • Randomized initiator ISN: phantun-dkms randomizes the initial sequence number; Phantun historically uses 0, which is easier for DPI to fingerprint.
  • Symmetric role model: phantun-dkms drops fixed client/server node roles and uses per-flow initiator / responder roles instead.
  • TCP-like keepalive behavior: phantun-dkms sends keepalive ACKs and tears flows down after repeated misses; that is another behavior difference that may break mixed-endpoint expectations.
  • Other protocol-level improvements: several behavior changes make accidental compatibility less likely, even though the fake-TCP packet shape stays close.

Short version: same broad wire shape, different enough behavior that you should treat this as kernel-to-kernel only unless proven otherwise.

If you know Phantun, here are the practical differences

Topic Phantun phantun-dkms
Where it runs User space Linux kernel module
Traffic model TUN interface plus routing/NAT around it Direct interception of selected existing UDP sockets
Process model Separate client/server binaries No separate daemon; same module runs on both hosts
Node roles Fixed client and server No node-level client/server split; each flow has initiator/responder only
Application integration App talks through Phantun’s TUN topology App keeps its existing UDP socket
Traffic selection Bind/listen topology around TUN endpoints Selector-based ownership via managed_local_ports and/or managed_remote_peers
Full-mesh setup Configure a tunnel for each node pair, so a mesh grows O(n²) Configure selectors once per node; no per-pair tunnels
Firewall/NAT plumbing Requires TUN-side DNAT/SNAT/masquerade setup No Phantun-specific TUN DNAT/masquerade plumbing; module stays on the normal host path and integrates with conntrack
Inbound raw UDP policy Ownership handled by TUN topology Selector-matched raw inbound UDP is dropped on non-loopback ingress
Liveness / keepalive No matching kernel-module keepalive behavior here TCP-like keepalive ACKs plus missed-keepalive teardown
Protocol compatibility Phantun contract Mixed Phantun / phantun-dkms use is untested and likely not seamless
Address family IPv4 and IPv6 IPv4 and IPv6, with ip_families=both/ipv4/ipv6 selection

What still feels like Phantun

The core fake-TCP shape is still familiar:

  • strict 3-way handshake
  • data rides in TCP ACK packets carrying payload
  • seq / ack track payload bytes
  • no FIN close state machine
  • RST is the teardown/error signal

So if you already understand why Phantun works on the wire, you already understand most of the packet-level idea here.

Can it coexist with Phantun?

Yes.

phantun-dkms explicitly ignores traffic on loopback devices, including UDP between local loopback endpoints. That means a local Phantun setup using 127.0.0.1 can coexist without the kernel module trying to steal it.

How to think about this project

Instead of creating a TUN interface and routing traffic through it, phantun-dkms does this:

Pick the UDP traffic to own, steal it in kernel, send fake TCP on the wire, turn inbound fake TCP back into UDP before the local app sees it.

That is the whole model.

When this is useful

Use this when you want Phantun-like fake TCP, but you want:

  • no TUN device
  • no user-space forwarding daemon
  • your app to keep its real UDP socket
  • explicit control over which local ports or remote peers get translated

Typical target: WireGuard or wireguard-go.

Quick start

Build & Install from source code

make
sudo make modules_install

The static Makefile bootstraps ./autogen.sh and ./configure only when their generated files are missing or stale.

If kernel autodetection fails, pass KDIR=/path/to/kernel/build to make.

Build release artifacts locally

make dkms
make dkms-deb
make dkms-rpm

Install from DKMS package (latest release)

  • Arch Linux: Install AUR package phantun-dkms
  • Debian/Ubuntu: Download and install .deb file from latest release
  • Fedora/RHEL/Rocky: Download and install the .rpm file from latest release
  • Other Distributions: Extract .tar.gz dkms tarball from latest release to /usr/src/phantun-0.x.y, and run dkms add manually

Install on NixOS from the flake

Add the flake input:

inputs.phantun-dkms = {
  url = "github:bjin/phantun-dkms";
  inputs.nixpkgs.follows = "nixpkgs";
};

Then import the NixOS module and configure the kernel parameters with typed options:

{
  imports = [ inputs.phantun-dkms.nixosModules.default ];

  services.phantun = {
    enable = true;
    loadOnBoot = true;
    managedRemotePeers = [
      "198.51.100.20:51820"
      "[2001:db8::20]:51820"
    ];
    handshakeRequest = "hello";
    handshakeResponse = "base64:d29ybGQ=";
    keepaliveIntervalSec = 15;
    keepaliveMisses = 3;
  };
}

loadOnBoot = false leaves the package and generated modprobe configuration available for manual modprobe phantun, but does not add phantun to boot.kernelModules. Set services.phantun.managedNetns = "all"; when the managed application intentionally runs outside the initial network namespace.

The nixpkgs.follows line makes this module use the consumer flake’s nixpkgs input instead of keeping a second transitive nixpkgs pinned by this repository.

Simplest load: own one local UDP port

sudo modprobe phantun managed_local_ports=51820

Own one exact remote peer instead

sudo modprobe phantun managed_remote_peers=198.51.100.20:51820
sudo modprobe phantun managed_remote_peers='[2001:db8::20]:51820'

Require both local port and remote peer

sudo modprobe phantun \
  managed_local_ports=51820 \
  managed_remote_peers=198.51.100.20:51820

Traffic ownership model

The module only touches traffic that matches one or both selector lists.

Selector Meaning Typical use
managed_local_ports Own this local UDP/TCP port “Translate my local WireGuard listen port”
managed_remote_peers Own this exact remote IPv4:port or [IPv6]:port “Translate only traffic for this peer”

Rules:

  • At least one selector list must be non-empty.
  • If you configure both, both must match.
  • Selector ownership applies only to non-loopback traffic.
  • Raw inbound UDP that matches the selectors is dropped on non-loopback ingress so traffic is not delivered both as raw UDP and translated UDP.
  • ip_families=both|ipv4|ipv6 selects which IP families are translated; it defaults to both. On kernels without IPv6 support, default both degrades to IPv4-only with a warning, while explicit ipv6 is rejected.
  • IPv6 managed_remote_peers entries must be bracketed, for example [2001:db8::20]:51820; unbracketed IPv6 endpoints are rejected as ambiguous.
  • managed_remote_peers is exact-address matching. If a remote host rotates its public source address, including IPv6 privacy-address rotation, update the configured peer address or use managed_local_ports for server-like endpoints that should accept any remote address on the owned local port.
  • IPv6 link-local endpoint addresses are not supported. Selector-matched traffic with a link-local local or remote endpoint is rejected instead of translated.

Namespace attachment

managed_netns=init|all controls where the module attaches before selectors are evaluated:

Value Meaning
init Default. Attach only to the initial network namespace (init_net), which is the normal host namespace.
all Compatibility mode. Attach to every network namespace created through Linux pernet hooks.

This boundary controls which namespaces receive flow tables, topology notifiers, optional reserved TCP sockets, and IPv4/IPv6 netfilter hooks. Inside each attached namespace, managed_local_ports and managed_remote_peers still decide traffic ownership. If your UDP application intentionally runs in a separate network namespace, container namespace, or test namespace, set managed_netns=all.

Selector modes

Mode What you set Tradeoff
Local-only managed_local_ports Easiest and closest to Phantun’s server-side selector model: own traffic by local service port.
Peer-only managed_remote_peers Closest to Phantun’s client-side selector model: own traffic by chosen remote peer. Inbound TCP ownership becomes broad for that remote IPv4:port or [IPv6]:port, so use only when that peer is dedicated to this translator.
Intersection Both Most explicit and usually safest.

Optional local TCP reservation for local-only mode

managed_local_ports tells phantun-dkms which traffic to intercept. It does not automatically make the module the real kernel TCP owner of that port.

That matters in local-only mode (managed_local_ports set, managed_remote_peers empty): inbound fake TCP for the chosen port is intercepted before the normal TCP stack sees it, but another application can still successfully bind() / listen() on the same TCP port. The result is misleading ownership: the application appears to own the port, yet the fake-TCP traffic you expected to reach it is intercepted by phantun-dkms first.

For a WireGuard-server-style deployment, this means managed_local_ports=51820 alone can shadow another TCP listener on port 51820 even though the listener started successfully. If that same host or namespace also runs user-space Phantun on 127.0.0.1:51820, the default behavior is still safe because loopback traffic is ignored.

managed_remote_peers narrows ownership to an exact remote IPv4:port or [IPv6]:port, so unrelated listeners on the same local port are not broadly shadowed in the same way. That is why reserved_local_ports is only honored in pure local-only mode.

reserved_local_ports is optional and defaults to empty. The special values off and empty input both disable it.

Value Meaning
empty / omitted disabled (default)
off disabled
comma-separated ports (max 64) reserve only the listed ports that also appear in managed_local_ports
all reserve every effective managed_local_ports entry

Rules and trade-offs:

  • only honored when managed_local_ports is set and managed_remote_peers is empty
  • reservation is attempted in selected namespaces only
  • reservation uses wildcard kernel binds on enabled families: 0.0.0.0:<port> for IPv4 and [::]:<port> for IPv6
  • wildcard reservation also blocks loopback listeners on the same port in that namespace
  • if a port is already occupied, the module stays active and logs that truthfully instead of failing the load

Use reserved_local_ports only when you want phantun-dkms to proactively claim those fake-TCP ports and you are willing to give up loopback TCP coexistence on those same ports in selected namespaces. If you want a local WireGuard server and a user-space Phantun process to share the host by using 127.0.0.1:<port>, keep this parameter unset.

Everyday parameters

Selectors and attachment

Parameter Type Default Meaning
managed_local_ports integer array, max 64 empty Local ports the module owns. For WireGuard, usually the local listen port.
managed_remote_peers string array, max 64 empty Exact peers in x.y.z.w:p or [IPv6]:p form.
ip_families string both Enabled translation families: both, ipv4, or ipv6.
managed_netns string init Namespace attachment scope: init for the initial network namespace only, or all for every network namespace.

Validation rules:

  • managed_local_ports: 1..65535
  • managed_netns: init or all
  • managed_remote_peers: valid IPv4:port or bracketed [IPv6]:port
  • at least one selector list required

Optional local TCP reservation

Parameter Type Default Meaning
reserved_local_ports string empty Optional local-only TCP reservation set. Empty or off disables it, all reserves every managed_local_ports entry, and a comma-separated list of up to 64 ports reserves only the listed managed_local_ports subset. Ignored unless managed_local_ports is set and managed_remote_peers is empty.

Optional timing and behavior

Parameter Default Meaning
handshake_request empty Optional initiator payload sent as first fake-TCP payload.
handshake_response empty Optional responder payload; effective only when handshake_request is also set.
handshake_timeout_ms 1000 Handshake retransmit timeout.
handshake_retries 6 Maximum handshake retry count before RST teardown.
keepalive_interval_sec 30 Idle period before sending keepalive ACK.
keepalive_misses 3 Unanswered keepalives allowed before teardown.
hard_idle_timeout_sec 300 Hard upper bound for idle flow lifetime.
reopen_guard_bytes 4194304 Minimum sequence-space distance before reopening same tuple; accepts 0..1073741823 and rejects values >= 1073741824.
half_open_limit 4096 Maximum concurrent half-open flows per network namespace. New SYN-created or outbound half-open flows beyond this limit are rejected until existing half-open flows establish or time out.
replacement_quarantine_ms 3000 Previous-generation quarantine window after tuple replacement. Matching old-generation packets are silently dropped during this window.
replacement_protect_ms 0 (auto) Established-initiator bare-SYN replacement protection window. During the window, aligned bare replacement SYNs to an established initiator are silently dropped to suppress stale simultaneous-initiation loser SYNs. After the window expires, normal replacement handling resumes.

Shaping payload formats

handshake_request and handshake_response accept:

Form Example Meaning
Plain string HELLO Send bytes as-is
Hex hex:deadbeef Decode remaining text as hex
Base64 base64:YWJj Decode remaining text as Base64 when the target kernel supports it

Malformed hex: payloads reject module load. Malformed base64: payloads reject module load when the target kernel provides Base64 decode; on kernels without in-kernel Base64 support, Base64 payloads are ignored with a kernel warning.

Payload length is validated against the fake-TCP payload limit for the smallest enabled family. When IPv6 is enabled, the IPv6 limit is used even if the operator expects only IPv4 flows.

Common setups

Own a WireGuard port

sudo modprobe phantun managed_local_ports=51820

Own a WireGuard port, but only for one peer

sudo modprobe phantun \
  managed_local_ports=51820 \
  managed_remote_peers=198.51.100.20:51820

Add optional request/response shaping hints

sudo modprobe phantun \
  managed_local_ports=51820 \
  managed_remote_peers=198.51.100.20:51820 \
  handshake_request=HELLO \
  handshake_response=WORLD

Persist across reboots

Create /etc/modprobe.d/phantun.conf:

options phantun \
  managed_local_ports=51820 \
  managed_remote_peers=198.51.100.20:51820

Then:

sudo modprobe phantun

Operational notes for Phantun users

Topic What changes here
No TUN plumbing Do not copy Phantun’s TUN DNAT/SNAT/masquerade setup into this project.
Conntrack The module stays on the normal host path and integrates with conntrack instead of creating a separate TUN routing topology.
Loopback Loopback traffic is still left alone by the data path, so default local Phantun-on-loopback setups can coexist. Only an effective reserved_local_ports wildcard bind in pure local-only mode blocks loopback listeners on the same port in a selected namespace; ignored or failed reservations do not.
MTU Same basic fake-TCP packet overhead as Phantun; Phantun’s MTU guidance still applies.
Handshake buffering During handshake, the module queues at most one outbound UDP packet per flow; later packets may be dropped and must rely on normal app retransmission.
Zero-length UDP A zero-length outbound UDP datagram on a managed tuple is consumed and counted as dropped; it does not create a flow and is not delivered, because fake-TCP data is carried in ACK payload bytes and the wire protocol has no representation for an empty datagram.
Shaping semantics handshake_request / handshake_response are hints, not a verified sub-protocol.
Keepalive phantun-dkms has TCP-like keepalive behavior; that is another reason mixed Phantun / phantun-dkms endpoints should not be assumed to interoperate.

Runtime stats

The module exports counters under:

/sys/module/phantun/stats/

Counters under /sys/module/phantun/stats/* are module-global and aggregate all managed network namespaces; they are not per-netns counters when managed_netns=all.

Example:

cat /sys/module/phantun/stats/flows_created
cat /sys/module/phantun/stats/rst_sent

Useful counters

Counters are monotonic. Some counters are aggregates and a single packet/event can increment both an aggregate and a more specific reason counter:

  • udp_queue_full_dropped, udp_raw_inbound_dropped, udp_translation_failed_dropped, and udp_reinject_failed_dropped are also counted in udp_packets_dropped.
  • tcp_misaligned_syn_rejected and tcp_unknown_tuple_rejected are also counted in tcp_protocol_rejected.
  • oversized_payloads_dropped covers both directions: outbound oversized UDP or established sends whose generated fake TCP exceeds the current route MTU are also counted in udp_packets_dropped; inbound oversized fake-TCP is also counted in tcp_protocol_rejected when the module rejects it with RST.
  • These reason counters are useful labels, not a full partition. They will not always add up to the aggregate counter.
Stat file Meaning
Flow lifecycle and capacity
flows_created Flow objects successfully inserted into the flow table.
flows_established Flows that reached ESTABLISHED.
flows_current Flow objects currently present in the flow table.
half_open_rejected Valid half-open openers rejected because the per-netns half-open limit was full.
handshake_retries_exhausted Half-open flows torn down after the handshake retransmit budget ran out.
established_liveness_timeouts Established flows torn down after missing too many keepalives.
Replacement and simultaneous-init recovery
replacements_accepted Established flows that accepted a valid bare, aligned replacement SYN on the same tuple.
replacement_quarantine_dropped Delayed previous-generation packets silently dropped during the replacement quarantine window. Bare SYNs are not quarantine drops.
replacement_protect_dropped Established initiator replacement SYNs silently dropped while the replacement-protect window was active.
retired_evicted Best-effort retired flow metadata records evicted because a hash bucket reached its cap.
collisions_won Simultaneous-initiation collisions where the local side kept initiator role.
collisions_lost Simultaneous-initiation collisions where the local side switched to responder role.
Shaping and control payloads
request_payloads_injected handshake_request payloads injected by the module.
response_payloads_injected handshake_response payloads injected by the module.
shaping_payloads_dropped Reserved first-payload slots hidden from the UDP app by handshake shaping.
RST and UDP packet accounting
rst_sent Fake-TCP RST packets successfully sent. A RST is often a reaction to a rejection or teardown; use the rejection/drop counters to tell why it was sent.
idle_acks_suppressed Immediate pure ACKs skipped for inbound established payloads because the flow recently sent local UDP payload within the short idle-ACK suppression window.
route_cache_hits Established fake-TCP payload sends that reused the per-flow dst cache after dst_check() accepted it.
route_cache_misses Established fake-TCP payload sends that needed a fresh route lookup because no matching valid cached dst was available.
udp_packets_queued Selected outbound UDP packets accepted into a flow’s one-skb queue. Queue-full drops are counted separately.
udp_packets_dropped Aggregate UDP datagrams consumed by the module and not successfully queued, translated, or reinjected.
udp_queue_full_dropped Selected outbound UDP dropped because the flow’s one-skb queue was already occupied. Also increments udp_packets_dropped.
udp_raw_inbound_dropped Selector-owned raw inbound UDP intentionally dropped in PRE_ROUTING to avoid raw and translated duplicate delivery. Also increments udp_packets_dropped.
udp_translation_failed_dropped Selected outbound UDP consumed after local translation/open/send plumbing failed. Also increments udp_packets_dropped; half-open admission-limit rejects are counted by half_open_rejected instead.
udp_reinject_failed_dropped Inbound fake-TCP payloads dropped because local UDP reinjection failed under transient pressure. Also increments udp_packets_dropped.
Fake-TCP rejection and validation
tcp_protocol_rejected Aggregate owned fake-TCP packets rejected as invalid for the current protocol/state-machine context. Check narrower counters below for common reasons.
tcp_misaligned_syn_rejected Owned bare SYNs rejected because seq % 4095 != 0. Also increments tcp_protocol_rejected.
tcp_unknown_tuple_rejected Owned fake-TCP packets with no live flow rejected because they were not valid new openers. Also increments tcp_protocol_rejected; misaligned bare SYNs use tcp_misaligned_syn_rejected instead.
bad_checksum_dropped Fake-TCP packets dropped due to invalid TCP checksum. These are not counted as protocol rejections.
oversized_payloads_dropped Payloads too large for module limits, plus established sends whose generated fake TCP exceeds the current route MTU. Outbound oversized UDP also increments udp_packets_dropped; inbound oversized fake-TCP also increments tcp_protocol_rejected when rejected with RST.

Build, reload, unload

Task Command
Build make
Refresh compile database make compile_commands
Install module sudo make modules_install
Load module sudo modprobe phantun ...
Unload module sudo rmmod phantun

Limits and current status

  • IPv4 and IPv6 are supported, with ip_families=both|ipv4|ipv6 selecting active translation families.
  • By default, hooks are installed only in the initial network namespace. Use managed_netns=all for namespace/container workloads that need translation outside init_net.
  • Linux kernel compatibility: from 5.10 to 7.2.
  • Mixed Phantun / phantun-dkms deployments are untested and should be treated as likely non-seamless.
  • No FIN close state machine.
  • This is a kernel-to-kernel protocol variant in practice, even though the basic packet shape stays close to Phantun.
  • First payloads reserved for shaping may be intentionally hidden from the UDP application.
  • Missing, delayed, duplicated, or lost shaping payloads do not by themselves fail the connection.

For protocol internals and state-machine details, see DESIGN.md.

License

Project license: GPL-2.0-or-later, see LICENSE file

评论

暂无评论