01 / 设计目标与技术选型

SharkLan 旨在解决传统 VPN 依赖中心服务器、单点故障、配置复杂的问题。其核心目标:

  • 去中心化:所有节点对等,无中央协调器,通过 DHT 或手动连接实现自发现。
  • 安全通信:节点间所有流量均使用 libp2p 的 Noise 加密 传输,同时节点本地密钥以 Argon2 + AES-GCM 加密存储。
  • 透明代理:在用户侧创建 TUN 虚拟网卡,使应用程序无感接入虚拟局域网,IP 包自动路由。
  • 轻量可移植:纯 Rust 实现,借助 Tokio 异步运行时,单进程即可运行。

技术选型方面,SharkLan 大量使用了以下生态组件:

  • libp2p:模块化 P2P 网络栈,提供传输、加密、多路复用、协议协商等基础能力。
  • QUIC:基于 UDP 的可靠传输,低延迟、多路复用,天然支持 NAT 穿透。
  • Tokio:异步运行时,支撑高并发事件循环。
  • wintun(Windows) / tun(Linux):虚拟网卡驱动,实现数据面注入。
  • Argon2 + AES-GCM:密钥派生与对称加密,保障本地密钥存储安全。
KEY DESIGN

SharkLan 将控制面(节点管理、路由同步)和数据面(IP 包转发)分离,控制面基于 libp2p 的 request-response 协议,数据面直接封装 IPv4 包通过同样的协议传输,实现了简洁而高效的统一通道。

02 / 节点身份与密钥管理

每个 SharkLan 节点拥有唯一的 PeerId,由 Ed25519 公钥派生。私钥用于 libp2p 的身份认证和 Noise 握手。

密钥存储

为了保护私钥,SharkLan 在首次启动时生成密钥对,并使用用户提供的密码进行加密保存。加密流程如下:

  • 使用 Argon2id(64 MB 内存、3 轮迭代)从密码派生出 32 字节 AES 密钥。
  • 生成随机 16 字节 Salt 和 12 字节 Nonce。
  • 使用 AES-256-GCM 加密 Ed25519 私钥的 Protobuf 编码。
  • 存储格式为 [salt (16) | nonce (12) | ciphertext],确保每次加密结果不同。

解密时校验数据长度,若密码错误则解密失败,有效防止离线暴力破解(Argon2 设计具有高计算成本)。

// keypair.rs 核心摘要 pub fn get_or_create_keypair(data_dir: &Path) -> Result<identity::Keypair> { let key_path = data_dir.join("keypair"); let password = get_password()?; if key_path.exists() { let encrypted = fs::read(&key_path)?; let plaintext = decrypt_data(&encrypted, &password)?; return identity::Keypair::from_protobuf_encoding(&plaintext); } let keypair = identity::Keypair::generate_ed25519(); let plaintext = keypair.to_protobuf_encoding()?; let encrypted = encrypt_data(&plaintext, &password)?; fs::write(&key_path, encrypted)?; // 权限设为 600 (Unix) Ok(keypair) }

03 / 控制协议与节点发现

SharkLan 在 libp2p 之上自定义了三种 request-response 协议,分别用于握手节点发现数据转发

协议定义

  • Hello/sharklan/control/1.0.0):连接建立后双方交换版本、节点名称、虚拟 IP,并确认连接。
  • KnownPeers/sharklan/peers/1.0.0):增量同步已知节点列表,每次请求携带 since_version(上次收到的最新更新时间戳),服务端只返回比该版本新的记录,大幅减少冗余传输。
  • TunPacket/sharklan/tun/1.0.0):封装一个完整的 IPv4 数据包(raw bytes),目标节点收到后将其写入本地 TUN 设备,实现数据面路由。
  • NodeLeaving:优雅退出时广播通知,帮助其他节点立即标记离线,避免超时等待。

节点发现与自动拨号

节点发现采用 “启动时从磁盘恢复 + 运行时主动拉取 + 自动拨号” 三级机制:

  1. 持久化:所有已知节点信息(PeerId、地址、虚拟 IP、最后更新时间等)定期序列化到本地 peers.bin,重启后恢复。
  2. 主动同步:每 30 秒向当前所有已连接节点发起 KnownPeers 请求,获取增量更新,并合并到本地 PeerStore。
  3. 自动拨号:对于离线但地址已知且最近活跃过的节点,系统会每隔 30 秒尝试重连(退避机制),尝试恢复连接。

这一设计使得网络无需中央追踪器,节点间通过相互交换信息就能逐渐发现全网拓扑。

增量同步的版本控制

每个节点为每个对端维护一个 sync_version,即该对端上次返回的全局版本号。当本地 PeerStore 中任何记录更新时,全局版本号(最大 last_updated)递增。下次请求时带上该版本,服务端只返回 last_updated > since_version 的记录,同时返回新的全局版本号。这种方式在网络规模较大时依然保持高效。

// network.rs 中增量导出实现 pub fn export_since(&self, since: u64, local: &PeerId) -> (u64, Vec<protocol::PeerInfo>) { let mut version = 0u64; let peers = self.peers .values() .filter(|info| &info.peer_id != local) .inspect(|info| version = version.max(info.last_updated_secs())) .filter(|info| info.last_updated_secs() > since) .map(protocol::PeerInfo::from) .collect(); (version, peers) }

04 / 数据面:TUN 设备与 IP 转发

数据面是 SharkLan 的“最后一公里”,负责将来自本机应用程序的 IP 包转发到对应节点,并将远程节点发来的 IP 包注入本机网络栈。

Windows 实现 (wintun)

在 Windows 上,SharkLan 使用 wintun 驱动创建虚拟网卡,设置 IP 地址(如 100.64.0.1/10),并启动两个阻塞线程分别负责读包写包。为了与异步主循环融合,通过 mpsc 通道将阻塞 I/O 转化为异步事件:

  • 读线程:循环调用 session.receive_blocking(),收到数据后通过 from_tun_tx 发送给主循环的 tun_rx
  • 写线程:循环从 to_tun_rx 接收数据,使用 allocate_send_packet 分配缓冲并写入网卡。

主循环在每次迭代中优先检查 tun_rx,若收到 IP 包,则解析目的 IP,并在 PeerStore 中查找对应的节点(通过虚拟 IP 映射),然后调用 send_request(Request::TunPacket) 将其转发给目标节点。

路由表(虚拟 IP 映射)

每个节点可以配置一个虚拟 IP(如 --ip 100.64.0.1),并在 Hello 握手时告知对端。PeerStore 维护 virtual_ip → PeerId 的映射,供数据面查询。若目的 IP 不在映射表中,则丢弃包并记录日志。

性能考量

SharkLan 对每个 IP 包都进行完整的 libp2p 请求-响应往返,这并非性能最优(更高效的做法是使用流式传输),但实现简单且适用于小规模网络(< 50 节点)。此外,通过重用 QUIC 连接,避免了重复握手开销。

// 数据包转发核心逻辑 (network.rs) fn forward_tun_packet(&mut self, data: Vec<u8>) { let dest = parse_ipv4_destination(&data).unwrap(); let target = match self.peer_store.find_by_ip(&dest) { Some(info) => info.peer_id, None => return, }; if !self.swarm.is_connected(&target) { return; } self.swarm .behaviour_mut() .request_response .send_request(&target, Request::TunPacket { data }); }

05 / 持久化与状态恢复

为了在重启后快速恢复网络状态,SharkLan 将 PeerStore 中所有节点的 持久化字段(PeerId、地址、虚拟 IP、版本、最后更新等)序列化为二进制格式(使用 bincode)存入 peers.bin。连接状态(在线/离线)和拨号尝试时间等瞬时信息不保存。

节点启动时调用 restore_known_peers() 加载文件,若文件不存在则视为空列表。之后通过定时维护(每 15 秒)触发 save_peers(),仅当 dirty 标志为 true 时执行写入,避免频繁磁盘 I/O。

此外,优雅退出时(Ctrl+C 或输入 exit),节点会向所有已连接节点发送 NodeLeaving 请求,并等待 500ms 让请求发出,然后保存 PeerStore 并退出。这样其他节点能立即获知离线,无需等待超时(默认 60 秒)。

06 / 安全与加密

SharkLan 在多个层次上保障安全:

  • 传输层:libp2p 默认使用 Noise 协议(基于 xx 握手模式)进行密钥交换和会话加密,所有控制协议和数据包均在加密通道中传输。
  • 身份认证:每个节点持有 Ed25519 密钥对,PeerId 即为公钥的哈希,连接建立时自动验证对方身份,防止中间人攻击。
  • 本地密钥保护:私钥使用用户提供的密码加密存储(Argon2 + AES-GCM),即便磁盘泄露,未授权者也无法获取私钥。

值得注意的是,数据面(TunPacket)并未在应用层额外加密,因为 libp2p 的传输层已经提供了完整的机密性和完整性。这简化了设计,并避免了双重加密带来的性能损耗。

07 / 命令行与用户体验

SharkLan 提供了简洁的交互式命令行,方便用户管理连接:

  • connect <multiaddr> —— 手动连接指定节点(如 /ip4/192.168.1.10/udp/5000/quic-v1/p2p/12D3KooW...)
  • peers —— 列出所有已知节点及其状态(在线/离线)、虚拟 IP。
  • peersync —— 手动触发一次已知节点同步,适用于首次加入网络或需要立即更新路由表。
  • exit —— 优雅退出,通知其他节点。

启动参数支持指定节点实例名称(用于多开)和虚拟 IP,使得在同一台机器上运行多个节点成为可能(只要 IP 不冲突)。

BUILD & RUN

项目使用 Cargo 构建,依赖 rustlsring 等纯 Rust 加密库,编译后单二进制文件约 10 MB。
运行示例:cargo run -- --node alice --ip 100.64.0.1

08 / 总结与展望

SharkLan 是一个完整且具有教育意义的 P2P VPN 实现,它展示了如何使用 libp2p 快速构建去中心化网络应用,同时也体现了 Rust 在系统编程领域的优势(内存安全、高性能、异步)。

目前项目尚在积极开发中,核心数据面与控制面逻辑已基本跑通,但距离生产稳定版本仍有不少打磨工作(如跨平台适配、性能优化等)。后续计划增加:

  • Linux / macOS TUN 支持(已预留接口,仅需替换 tun.rs 实现)。
  • 基于 Kademlia DHT 的自动节点发现,代替手动连接。
  • 支持 IPv6 包转发和更灵活的路由策略。
  • 性能优化:引入流式传输或 UDP 直通,减少每个包的 RTT 开销。

如果你对 P2P 网络或 Rust 异步编程感兴趣,SharkLan 是一个不错的参考项目。待稳定后将择机开源,欢迎持续关注。

PROJECT STATUS

项目仍处于早期开发阶段,代码仓库暂未公开。
待核心功能稳定、跨平台适配完成后,将择机开源。欢迎持续关注。