Add deployable QUIC P2P hole-punch coordination server

This commit is contained in:
LIm1tZz7 committed 2026-10-12 20:18:51 +08:00
commit 328aad2b00
13 files changed
+4290

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
target
data
.git
.env
*.key
+4
View File
@@ -0,0 +1,4 @@
/target/
/data/
*.key
.env
Generated
+3596
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "chatmask-server"
version = "0.1.0"
edition = "2021"
rust-version = "1.94"
autotests = false
[[test]]
name = "coordination"
path = "tests/coordination.rs"
[dependencies]
clap = { version = "4.5", features = ["derive"] }
futures = "0.3"
libp2p = { version = "=0.57.0", default-features = false, features = ["tokio", "tcp", "noise", "yamux", "ed25519", "request-response", "cbor", "macros", "quic"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "time", "net"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[profile.release]
opt-level = "s"
lto = "thin"
codegen-units = 1
strip = true
+15
View File
@@ -0,0 +1,15 @@
FROM rust:1.94-bookworm AS build
RUN apt-get update && apt-get install -y --no-install-recommends cmake && rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --locked --release
FROM debian:bookworm-slim
RUN groupadd --gid 10001 chatmask && useradd --uid 10001 --gid chatmask --no-create-home chatmask && mkdir /data && chown chatmask:chatmask /data
COPY --from=build /build/target/release/chatmask-server /usr/local/bin/chatmask-server
USER chatmask
EXPOSE 7001/udp
STOPSIGNAL SIGTERM
ENTRYPOINT ["chatmask-server"]
CMD ["--key", "/data/server.key"]
+29
View File
@@ -0,0 +1,29 @@
# ChatMaskServer 后续开发提示词
请基于当前只交换打洞信令的协调服务器实现以下未来需求。以下仅为计划,本阶段不要创建占位接口、后台服务或复杂架构。按实际需求逐项实现、测试并从本文件删除已完成项。
## 节点探测
- 支持节点可达性及地址有效性探测,区分已认证身份、当前观察地址与短期可达结果。
- 使用有期限的探测结果,处理断线、地址变化和重复登记;失败不代表身份不存在。
- 设定探测并发、频率、超时和包大小上限,防止被利用为任意地址扫描或 UDP 放大工具。
## 节点发现
- 让节点能找到共同的协调服务器及对端最新可用地址候选,明确可信来源、有效期和选择规则。
- 如需要服务器间发现或联合查询,先定义协议及身份核验,再加入有限范围的节点互通;避免全网广播及无限递归查询。
- 支持节点退出、服务器故障、重复结果去重和版本兼容。
## Tracker 提供 peer 列表
- 实现轻量 tracker,按明确范围提供已登记且允许被发现的 peer 列表。
- 返回 PeerId、必要的短期地址候选和有效期,不传播用户本地名称、聊天内容或消息历史。
- 支持分页、结果数量上限、查询速率限制和过期清理,不一次返回全部在线节点。
- 明确公开发现的选择加入机制与地址披露规则;获知 PeerId 不等于有权限获得其所有地址。
- 列表只反映短期登记,不承诺节点一定可达或双方一定能打洞。
## 多服务器与验证
- 定义共同协调节点的选择和故障切换;保持服务器间及客户端消息去重和资源上限。
- 验证多服务器、不同 NAT、网络变化及恶意登记/查询场景;没有实际公网验证时明确标注限制。
- 所有功能保持无聊天数据中继、无离线代收、无消息内容存储。只有实际容量和持久化需求达到必要程度时才评估数据库。
+76
View File
@@ -0,0 +1,76 @@
# ChatMask 打洞协调服务器
只进行短期身份登记、来源地址观察和双端打洞信令。没有中继、聊天内容转发或离线代收。客户端与服务器通过 QUIC 建立经过 PeerId 认证的加密控制连接;服务器从该连接获取 UDP 来源地址,无需单独的探测套接字。
## 部署
服务器需有公网 IP,并放行 UDP 7001(云服务器同时开放安全组)。
```sh
docker compose up -d --build
docker compose logs -f
```
Compose 使用非 root 用户、只读根目录、持久化 server-data 卷、256 MiB 内存限制和日志轮转。服务器密钥位于 `/data/server.key`;重启保持 PeerId。备份密钥,不公开,不用于两个并行实例。不要用 `docker compose down -v` 删除身份卷。
直接构建运行(Rust 1.94 或更新版本;本机还需要 C 编译器和 CMake 构建 QUIC 加密依赖):
```sh
cargo build --locked --release
./target/release/chatmask-server
```
Windows 使用 `target\release\chatmask-server.exe`。默认 `--listen /ip4/0.0.0.0/udp/7001/quic-v1`、`--key data/server.key`,可配置 `--max-clients`(256)和 `--ttl-seconds`(60)。IPv6 可用 `--listen /ip6/::/udp/7001/quic-v1`。Ctrl+C 或 Linux SIGTERM 正常退出;身份文件损坏时拒绝启动,不重新生成。
Linux 后台运行:
```sh
sudo useradd --system --no-create-home --shell /usr/sbin/nologin chatmask-server
sudo install -m 755 target/release/chatmask-server /usr/local/bin/chatmask-server
sudo install -m 644 deploy/chatmask-server.service /etc/systemd/system/chatmask-server.service
sudo systemctl daemon-reload
sudo systemctl enable --now chatmask-server
sudo journalctl -u chatmask-server -f
```
日志输出 `Control address: /ip4/.../udp/7001/quic-v1/p2p/服务器PeerId`。分发给用户时将监听地址 `0.0.0.0` 或内网地址换成真实公网 IP,保留端口和服务器 PeerId。
## ChatMask 客户端接入
```sh
chatmask-client --bind 0.0.0.0 --server /ip4/服务器公网IP/udp/7001/quic-v1/p2p/服务器PeerId
```
双方需登记到至少一个共同服务器;`--server` 可重复指定最多四个带 PeerId 的 IP/QUIC 地址。使用 `/chat 对方PeerId`,无需先保存对方地址。客户端优先尝试已保存直连地址;无地址或拨号失败时请求协调。服务器给双方发送同一轮通知,双方复用原 QUIC UDP 监听端口同时拨号,直连成功后继续现有端到端消息加密、确认和待发补发。
服务器不会进入联系人或聊天会话。服务器不可用或打洞失败时,消息保持本地 Pending,不自动改走服务器转发。
## 协议和资源
协议为 `/chatmask/punch/2.0.0`,CBOR request-response,类型见 `src/lib.rs`:
1. 客户端建立经过认证的 QUIC 控制连接并发出 Register。
2. 服务器回复 Registered(观察到的 UDP 来源 endpoint、有效期)。身份来自连接认证,地址来自实际连接,不接受客户端声称的任意 endpoint。
3. A 请求 Connect(B的PeerId)。双方已登记时向双方发出 Punch(对端身份、endpoint、attempt、最长 5000ms),并向 A 回复 Started。Started 仅表示信令已安排,不表示打洞成功。
4. 客户端响应 Ack,并从原端口进行有限次数的同时拨号。实际 QUIC 连接必须验证预期 PeerId,聊天继续原有消息加密握手。
5. 客户端按服务器有效期刷新登记,默认每20秒;60秒过期或断线时移除登记。登记仅在内存,重启后需重新登记。
默认最多256条已建立连接/登记、每身份一条控制连接、32个待完成入站连接;控制包2048字节上限、每连接8个并发请求子流、5秒请求超时。每身份发起协调的间隔至少2秒,重复登记不会绕过该限制。
这不是标准 STUN/ICE、libp2p DCUtR 或 TURN 服务。使用 QUIC 的已认证来源地址和复用端口进行协调打洞。NAT可能针对不同目标产生不同映射,严格 NAT、CGNAT 或防火墙仍可能阻止直连;不保证公网成功率,不提供中继兜底。连接对象会获知彼此 IP,这是直连需要的信息;客户端会拒绝屏蔽目标的打洞通知。
## 测试与后续计划
```sh
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
cargo fmt --check
```
服务器测试覆盖真实进程上的 QUIC 身份登记、来源地址与双端协调,以及容量、有效期、速率限制和密钥损坏拒绝。客户端项目另有需要本服务器二进制的真实双客户端加密消息测试。公网 NAT 成功率、Docker 和 Linux systemd 尚需实际部署验证。
未来节点探测、发现和 tracker peer 列表只列入 [PLAN.md](PLAN.md),没有实现占位接口。
## 旧部署升级
现在只开放 UDP 7001;不再需要 TCP7000、`--udp-listen`、旧 JSON probe/签名确认或 `/chatmask/punch/1.0.0`。请同时升级客户端与服务器。旧密钥仍可复用,旧中继、旧服务和容器需先停止以避免端口冲突;旧数据卷不自动删除。
+20
View File
@@ -0,0 +1,20 @@
services:
server:
build: .
restart: unless-stopped
ports:
- "7001:7001/udp"
volumes:
- server-data:/data
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
mem_limit: 256m
cpus: 1.0
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
server-data:
+25
View File
@@ -0,0 +1,25 @@
[Unit]
Description=ChatMask P2P hole-punch coordination
After=network-online.target
Wants=network-online.target
[Service]
User=chatmask-server
Group=chatmask-server
StateDirectory=chatmask-server
StateDirectoryMode=0700
ExecStart=/usr/local/bin/chatmask-server --key /var/lib/chatmask-server/server.key
Restart=on-failure
RestartSec=5
TimeoutStopSec=10
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
UMask=0077
MemoryMax=256M
TasksMax=128
LimitNOFILE=2048
[Install]
WantedBy=multi-user.target
+234
View File
@@ -0,0 +1,234 @@
use clap::Parser;
use libp2p::{
connection_limits, identity, request_response, swarm::NetworkBehaviour, Multiaddr, PeerId,
StreamProtocol, Swarm, SwarmBuilder,
};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
error::Error,
fs::{self, OpenOptions},
io::{Read, Write},
net::SocketAddr,
path::Path,
time::{Duration, Instant},
};
pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>;
pub const PROTOCOL: &str = "/chatmask/punch/2.0.0";
#[derive(Parser, Debug)]
#[command(version, about = "ChatMask QUIC hole-punch coordination server")]
pub struct Args {
#[arg(long, default_value = "/ip4/0.0.0.0/udp/7001/quic-v1")]
pub listen: Multiaddr,
#[arg(long, default_value = "data/server.key")]
pub key: std::path::PathBuf,
#[arg(long, default_value_t = 256, value_parser = clap::value_parser!(u32).range(1..))]
pub max_clients: u32,
#[arg(long, default_value_t = 60, value_parser = clap::value_parser!(u32).range(10..3601))]
pub ttl_seconds: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Request {
Register,
Connect {
peer: String,
},
Punch {
peer: String,
endpoint: SocketAddr,
attempt: String,
timeout_ms: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Response {
Registered {
endpoint: SocketAddr,
ttl_seconds: u32,
},
Started {
attempt: String,
},
Ack,
Error {
message: String,
},
}
struct Registration {
endpoint: SocketAddr,
seen: Instant,
last_connect: Option<Instant>,
}
pub struct Registry {
clients: HashMap<PeerId, Registration>,
max: usize,
ttl: Duration,
}
impl Registry {
pub fn new(max: u32, ttl_seconds: u32) -> Self {
Self {
clients: HashMap::new(),
max: max as usize,
ttl: Duration::from_secs(ttl_seconds.into()),
}
}
pub fn remove(&mut self, peer: PeerId) {
self.clients.remove(&peer);
}
pub fn expire(&mut self) {
let ttl = self.ttl;
self.clients.retain(|_, r| r.seen.elapsed() < ttl);
}
pub fn register(&mut self, peer: PeerId, endpoint: SocketAddr) -> Response {
self.expire();
if !self.clients.contains_key(&peer) && self.clients.len() >= self.max {
return Response::Error {
message: "Server full".into(),
};
}
let last_connect = self.clients.get(&peer).and_then(|r| r.last_connect);
self.clients.insert(
peer,
Registration {
endpoint,
seen: Instant::now(),
last_connect,
},
);
Response::Registered {
endpoint,
ttl_seconds: self.ttl.as_secs() as u32,
}
}
pub fn connect(
&mut self,
source: PeerId,
target: PeerId,
) -> Result<(SocketAddr, SocketAddr, String)> {
self.expire();
if source == target {
return Err("Cannot connect to self".into());
}
let a = self.clients.get(&source).ok_or("Register first")?.endpoint;
let b = self
.clients
.get(&target)
.ok_or("Target not available")?
.endpoint;
let r = self.clients.get_mut(&source).expect("registered source");
if r.last_connect
.is_some_and(|t| t.elapsed() < Duration::from_secs(2))
{
return Err("Try again later".into());
}
r.last_connect = Some(Instant::now());
Ok((
a,
b,
identity::Keypair::generate_ed25519()
.public()
.to_peer_id()
.to_string(),
))
}
}
pub fn socket_address(address: &Multiaddr) -> Option<SocketAddr> {
use libp2p::multiaddr::Protocol;
let mut ip = None;
let mut port = None;
let mut quic = false;
for p in address.iter() {
match p {
Protocol::Ip4(v) => ip = Some(v.into()),
Protocol::Ip6(v) => ip = Some(v.into()),
Protocol::Udp(v) => port = Some(v),
Protocol::QuicV1 => quic = true,
_ => {}
}
}
if quic {
Some(SocketAddr::new(ip?, port?))
} else {
None
}
}
#[derive(NetworkBehaviour)]
pub struct Behaviour {
pub control: request_response::cbor::Behaviour<Request, Response>,
limits: connection_limits::Behaviour,
}
pub fn build(args: &Args, key: identity::Keypair) -> Result<Swarm<Behaviour>> {
let mut swarm = SwarmBuilder::with_existing_identity(key)
.with_tokio()
.with_quic()
.with_behaviour(|_| Behaviour {
control: request_response::cbor::Behaviour::with_codec(
request_response::cbor::codec::Codec::default()
.set_request_size_maximum(2048)
.set_response_size_maximum(2048),
[(
StreamProtocol::new(PROTOCOL),
request_response::ProtocolSupport::Full,
)],
request_response::Config::default()
.with_request_timeout(Duration::from_secs(5))
.with_max_concurrent_streams(8),
),
limits: connection_limits::Behaviour::new(
connection_limits::ConnectionLimits::default()
.with_max_pending_incoming(Some(32))
.with_max_pending_outgoing(Some(32))
.with_max_established(Some(args.max_clients))
.with_max_established_per_peer(Some(1)),
),
})?
.with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(120)))
.build();
swarm.listen_on(args.listen.clone())?;
Ok(swarm)
}
pub fn load_identity(path: &Path) -> Result<identity::Keypair> {
match fs::File::open(path) {
Ok(file) => {
let mut bytes = Vec::new();
file.take(4097).read_to_end(&mut bytes)?;
if bytes.len() > 4096 {
return Err("Identity file too large".into());
}
let key = identity::Keypair::from_protobuf_encoding(&bytes)?;
if key.key_type() != identity::KeyType::Ed25519 {
return Err("Expected an Ed25519 identity".into());
}
return Ok(key);
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
let mut builder = fs::DirBuilder::new();
builder.recursive(true).mode(0o700).create(parent)?;
}
#[cfg(not(unix))]
fs::create_dir_all(parent)?;
}
let key = identity::Keypair::generate_ed25519();
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path)?;
// Never replace an existing or damaged identity with a new identity silently.
file.write_all(&key.to_protobuf_encoding()?)?;
file.sync_all()?;
Ok(key)
}
#[cfg(test)]
#[path = "../tests/registry.rs"]
mod tests;
+58
View File
@@ -0,0 +1,58 @@
use chatmask_server::{Args, BehaviourEvent, Registry, Request, Response, Result};
use clap::Parser;
use futures::StreamExt;
use libp2p::{multiaddr::Protocol, request_response, swarm::SwarmEvent};
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let key = chatmask_server::load_identity(&args.key)?;
let peer = key.public().to_peer_id();
let mut swarm = chatmask_server::build(&args, key)?;
let mut registry = Registry::new(args.max_clients, args.ttl_seconds);
let mut tick = tokio::time::interval(std::time::Duration::from_secs(1));
let mut endpoints = HashMap::new();
let shutdown = shutdown();
tokio::pin!(shutdown);
println!("Server PeerId: {peer}");
loop {
tokio::select! {
result = &mut shutdown => { result?; println!("Shutting down"); break; },
_ = tick.tick() => registry.expire(),
event = swarm.select_next_some() => match event {
SwarmEvent::NewListenAddr { address, .. } => println!("Control address: {}", address.with(Protocol::P2p(peer))),
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => { if let Some(address) = chatmask_server::socket_address(endpoint.get_remote_address()) { endpoints.insert(peer_id, address); } },
SwarmEvent::ConnectionClosed { peer_id, .. } if !swarm.is_connected(&peer_id) => { registry.remove(peer_id); endpoints.remove(&peer_id); },
SwarmEvent::Behaviour(BehaviourEvent::Control(request_response::Event::Message { peer, message: request_response::Message::Request { request, channel, .. }, .. })) => {
let response = match request {
Request::Register => match endpoints.get(&peer) { Some(endpoint) => registry.register(peer, *endpoint), None => Response::Error { message: "QUIC connection required".into() } },
Request::Connect { peer: target } => match target.parse().map_err(|_| "Invalid target".into()).and_then(|target| registry.connect(peer, target).map(|pair| (target, pair))) {
Ok((target, (a, b, attempt))) => {
swarm.behaviour_mut().control.send_request(&target, Request::Punch { peer: peer.to_string(), endpoint: a, attempt: attempt.clone(), timeout_ms: 5000 });
swarm.behaviour_mut().control.send_request(&peer, Request::Punch { peer: target.to_string(), endpoint: b, attempt: attempt.clone(), timeout_ms: 5000 });
Response::Started { attempt }
},
Err(error) => Response::Error { message: error.to_string() },
},
Request::Punch { .. } => Response::Error { message: "Server-only request".into() },
};
let _ = swarm.behaviour_mut().control.send_response(channel, response);
},
SwarmEvent::ListenerError { error, .. } => return Err(error.into()),
SwarmEvent::ListenerClosed { reason, .. } => { reason?; return Err("Listener closed".into()); },
_ => {},
}
}
}
Ok(())
}
async fn shutdown() -> std::io::Result<()> {
#[cfg(unix)]
{
let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
tokio::select! { r = tokio::signal::ctrl_c() => r, _ = term.recv() => Ok(()) }
}
#[cfg(not(unix))]
tokio::signal::ctrl_c().await
}
+168
View File
@@ -0,0 +1,168 @@
use chatmask_server::{Request, Response, PROTOCOL};
use futures::StreamExt;
use libp2p::{
identity, noise, request_response, swarm::SwarmEvent, tcp, yamux, StreamProtocol, Swarm,
SwarmBuilder,
};
use std::{
fs,
io::{BufRead, BufReader},
process::{Child, Command, Stdio},
sync::mpsc,
time::Duration,
};
fn client(key: identity::Keypair) -> Swarm<request_response::cbor::Behaviour<Request, Response>> {
SwarmBuilder::with_existing_identity(key)
.with_tokio()
.with_tcp(
tcp::Config::default(),
noise::Config::new,
yamux::Config::default,
)
.unwrap()
.with_quic()
.with_behaviour(|_| {
request_response::cbor::Behaviour::new(
[(
StreamProtocol::new(PROTOCOL),
request_response::ProtocolSupport::Full,
)],
request_response::Config::default(),
)
})
.unwrap()
.build()
}
struct Server(Child);
impl Drop for Server {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
struct Directory(std::path::PathBuf);
impl Drop for Directory {
fn drop(&mut self) {
// The target is the exact unique temporary directory created by this test.
if self.0.starts_with(std::env::temp_dir()) {
let _ = fs::remove_dir_all(&self.0);
}
}
}
#[tokio::test]
async fn real_server_registers_observed_udp_addresses_signals_both_peers_and_clients_send_directly()
{
let dir = Directory(std::env::temp_dir().join(format!(
"chatmask-coordination-{}",
libp2p::PeerId::random()
)));
fs::create_dir(&dir.0).unwrap();
let mut server = Server(
Command::new(env!("CARGO_BIN_EXE_chatmask-server"))
.args(["--listen", "/ip4/127.0.0.1/udp/0/quic-v1"])
.arg("--key")
.arg(dir.0.join("server.key"))
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.unwrap(),
);
let (tx, rx) = mpsc::channel();
let stdout = server.0.stdout.take().unwrap();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
if tx.send(line.unwrap()).is_err() {
break;
}
}
});
let control = loop {
let line = rx.recv_timeout(Duration::from_secs(10)).unwrap();
if let Some(value) = line.strip_prefix("Control address: ") {
break value.parse::<libp2p::Multiaddr>().unwrap();
}
};
let key_a = identity::Keypair::generate_ed25519();
let key_b = identity::Keypair::generate_ed25519();
let a_peer = key_a.public().to_peer_id();
let b_peer = key_b.public().to_peer_id();
let server_peer = match control.iter().last().unwrap() {
libp2p::multiaddr::Protocol::P2p(peer) => peer,
_ => panic!(),
};
let mut a = client(key_a.clone());
let mut b = client(key_b.clone());
a.listen_on("/ip4/127.0.0.1/udp/0/quic-v1".parse().unwrap())
.unwrap();
b.listen_on("/ip4/127.0.0.1/udp/0/quic-v1".parse().unwrap())
.unwrap();
a.dial(control.clone()).unwrap();
b.dial(control).unwrap();
a.behaviour_mut()
.send_request(&server_peer, Request::Register);
b.behaviour_mut()
.send_request(&server_peer, Request::Register);
let (mut endpoint_a, mut endpoint_b) = (None, None);
tokio::time::timeout(Duration::from_secs(10), async {
while endpoint_a.is_none() || endpoint_b.is_none() {
tokio::select! {
event = a.select_next_some() => if let SwarmEvent::Behaviour(request_response::Event::Message { message: request_response::Message::Response { response: Response::Registered { endpoint, .. }, .. }, .. }) = event { endpoint_a = Some(endpoint); },
event = b.select_next_some() => if let SwarmEvent::Behaviour(request_response::Event::Message { message: request_response::Message::Response { response: Response::Registered { endpoint, .. }, .. }, .. }) = event { endpoint_b = Some(endpoint); },
}
}
}).await.unwrap();
let endpoint_a = endpoint_a.unwrap();
let endpoint_b = endpoint_b.unwrap();
a.behaviour_mut().send_request(
&server_peer,
Request::Connect {
peer: b_peer.to_string(),
},
);
let (mut notice_a, mut notice_b, mut started) = (None, None, None);
tokio::time::timeout(Duration::from_secs(10), async {
while notice_a.is_none() || notice_b.is_none() || started.is_none() {
tokio::select! {
event = a.select_next_some() => if let SwarmEvent::Behaviour(request_response::Event::Message { peer, message, .. }) = event {
assert_eq!(peer, server_peer);
match message {
request_response::Message::Request { request: Request::Punch { peer, endpoint, attempt, .. }, channel, .. } => {
assert_eq!(peer, b_peer.to_string()); assert_eq!(endpoint, endpoint_b); notice_a = Some(attempt);
a.behaviour_mut().send_response(channel, Response::Ack).unwrap();
},
request_response::Message::Response { response: Response::Started { attempt }, .. } => started = Some(attempt),
_ => {},
}
},
event = b.select_next_some() => if let SwarmEvent::Behaviour(request_response::Event::Message { peer: sender, message: request_response::Message::Request { request: Request::Punch { peer, endpoint, attempt, .. }, channel, .. }, .. }) = event {
assert_eq!(sender, server_peer); assert_eq!(peer, a_peer.to_string()); assert_eq!(endpoint, endpoint_a); notice_b = Some(attempt);
b.behaviour_mut().send_response(channel, Response::Ack).unwrap();
}
}
}
}).await.unwrap();
assert_eq!(notice_a, notice_b);
assert_eq!(notice_a, started);
assert_ne!(endpoint_a.port(), 0);
assert_ne!(endpoint_b.port(), 0);
}
#[test]
fn identity_is_stable_and_corruption_does_not_rotate_it() {
let dir =
Directory(std::env::temp_dir().join(format!("chatmask-key-{}", libp2p::PeerId::random())));
let path = dir.0.join("server.key");
let key = chatmask_server::load_identity(&path).unwrap();
assert_eq!(
key.public().to_peer_id(),
chatmask_server::load_identity(&path)
.unwrap()
.public()
.to_peer_id()
);
fs::write(&path, b"damaged").unwrap();
assert!(chatmask_server::load_identity(&path).is_err());
assert_eq!(fs::read(&path).unwrap(), b"damaged");
}
+35
View File
@@ -0,0 +1,35 @@
use super::*;
#[test]
fn authenticated_observed_endpoints_expire_and_capacity_and_retry_rates_are_bounded() {
let (a, b) = (PeerId::random(), PeerId::random());
let mut registry = Registry::new(2, 60);
let ep_a = "127.0.0.1:9000".parse().unwrap();
let ep_b = "127.0.0.1:9001".parse().unwrap();
assert!(registry.connect(a, b).is_err());
assert!(
matches!(registry.register(a,ep_a),Response::Registered{endpoint,..} if endpoint==ep_a)
);
registry.register(b, ep_b);
assert!(matches!(
registry.register(PeerId::random(), ep_a),
Response::Error { .. }
));
let (actual_a, actual_b, _) = registry.connect(a, b).unwrap();
assert_eq!((actual_a, actual_b), (ep_a, ep_b));
registry.register(a, ep_a);
assert!(registry.connect(a, b).is_err());
assert!(registry.connect(a, a).is_err());
registry.clients.get_mut(&b).unwrap().seen -= Duration::from_secs(61);
registry.expire();
assert!(registry.connect(a, b).is_err());
registry.remove(a);
assert!(registry.clients.is_empty());
}
#[test]
fn only_quic_addresses_produce_udp_candidates() {
assert!(socket_address(&"/ip4/127.0.0.1/tcp/9000".parse().unwrap()).is_none());
assert_eq!(
socket_address(&"/ip4/127.0.0.1/udp/9000/quic-v1".parse().unwrap()).unwrap(),
"127.0.0.1:9000".parse().unwrap()
);
}