use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use nfrp_common::auth::oidc::TokenSource;
use nfrp_common::config::ClientConfig;
use nfrp_common::vnet::{
encode_frame, open_tun, read_json_line, take_frame, Subnet, Tun, VnetRegister,
VnetRegisterResp, MAX_IP_PACKET,
};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::{debug, error, info, warn};
use crate::current_credential;
const TUN_NAME: &str = "nfrp%d";
const MAX_RESP_LINE: usize = 8 * 1024;
pub async fn run(cfg: Arc<ClientConfig>, token_source: Option<Arc<TokenSource>>) -> Result<()> {
let interval = Duration::from_secs(cfg.reconnect_interval.max(1));
let mut tun: Option<Tun> = None;
let mut current_spec: Option<String> = None;
loop {
match session(&cfg, &token_source, &mut tun, &mut current_spec).await {
Ok(()) => info!("VirtualNet 会话正常结束,{interval:?} 后重连"),
Err(e) => {
error!("VirtualNet 会话中断:{e:#}");
if e.to_string().contains("TUN") && tun.is_none() {
return Err(e);
}
warn!("{interval:?} 后重试");
}
}
tokio::time::sleep(interval).await;
}
}
async fn session(
cfg: &ClientConfig,
token_source: &Option<Arc<TokenSource>>,
tun: &mut Option<Tun>,
current_spec: &mut Option<String>,
) -> Result<()> {
let port = cfg.virtual_net.server_port;
if port == 0 {
anyhow::bail!(
"开了 [virtualNet] 但没写 serverPort —— 不知道服务端的 VirtualNet 端口是哪个。\
请在客户端配置里补上 `serverPort = <服务端 vnet_port>`。"
);
}
let server = format!("{}:{}", cfg.server_addr, port);
let addr = nfrp_common::util::resolve_addr(&server)
.await
.with_context(|| format!("解析 VirtualNet 服务端地址 {server} 失败"))?;
let mut stream = TcpStream::connect(addr)
.await
.with_context(|| format!("连接 VirtualNet 服务端 {addr} 失败"))?;
stream.set_nodelay(true).ok();
let ts = nfrp_common::util::now_unix_secs() as i64;
let cred = current_credential(cfg, token_source).await?;
let reg = VnetRegister {
client: cfg.client_id.clone(),
network: cfg.virtual_net.network.clone(),
address: cfg.virtual_net.address.clone(),
token: cred.wire_value(ts),
timestamp: ts,
};
stream.write_all(®.to_line()?).await?;
stream.flush().await?;
let mut resp_buf: Vec<u8> = Vec::with_capacity(256);
let resp_line = read_json_line(&mut stream, &mut resp_buf, MAX_RESP_LINE)
.await
.context("读 VirtualNet 注册应答失败(多半是认证失败或端口不对)")?;
let resp = VnetRegisterResp::from_line(&resp_line)?;
if !resp.ok {
anyhow::bail!("VirtualNet 注册被拒绝:{}", resp.error);
}
let ip: std::net::Ipv4Addr = resp
.address
.parse()
.with_context(|| format!("服务端返回的地址无法解析:{:?}", resp.address))?;
let sub = Subnet::parse(&resp.subnet)
.with_context(|| format!("服务端返回的网段无法解析:{:?}", resp.subnet))?;
let mtu = if resp.mtu == 0 { 1400 } else { resp.mtu };
let spec = format!("{ip}/{}", sub.prefix());
match tun {
None => {
let gw = resp.gateway.clone();
let dev = open_tun(TUN_NAME, mtu, Some(&spec), None).with_context(|| {
format!(
"创建虚拟网卡失败(需要 root 或 CAP_NET_ADMIN)。\
服务端分配的地址是 {spec},网关 {gw}"
)
})?;
info!(
dev = dev.name(),
%ip,
subnet = %sub,
%gw,
mtu,
"VirtualNet 网卡已就绪"
);
*current_spec = Some(spec);
*tun = Some(dev);
}
Some(dev) => {
if current_spec.as_deref() != Some(spec.as_str()) {
warn!(
old = ?current_spec,
new = %spec,
"重连后虚拟地址变了,正在更新网卡"
);
dev.set_address(&spec)?;
*current_spec = Some(spec);
}
debug!(dev = dev.name(), %ip, "VirtualNet 重连成功");
}
}
relay(stream, tun.as_mut().expect("上面刚保证过")).await
}
async fn relay(stream: TcpStream, tun: &mut Tun) -> Result<()> {
let (mut sock_rd, mut sock_wr) = stream.into_split();
let mut tun_buf = vec![0u8; MAX_IP_PACKET];
let mut sock_buf = vec![0u8; MAX_IP_PACKET];
let mut pending: Vec<u8> = Vec::new();
loop {
tokio::select! {
n = tun.read(&mut tun_buf) => {
let n = n.context("读 TUN 失败")?;
if n == 0 {
anyhow::bail!("TUN 返回 0 字节(设备被删除?)");
}
let frame = encode_frame(&tun_buf[..n])?;
sock_wr.write_all(&frame).await.context("写 VirtualNet socket 失败")?;
sock_wr.flush().await.ok();
}
n = sock_rd.read(&mut sock_buf) => {
let n = n.context("读 VirtualNet socket 失败")?;
if n == 0 {
anyhow::bail!("服务端关闭了 VirtualNet 连接");
}
pending.extend_from_slice(&sock_buf[..n]);
while let Some(pkt) = take_frame(&mut pending)? {
tun.write_all(&pkt).await.context("写 TUN 失败")?;
}
}
}
}
}