Ferrit Explore
中文·繁體·EN·日本語 Sign in Register
cielxl / magpie / src / main.rs
//! 鹊桥 (Magpie) — a general-purpose WireGuard VPN client, in Rust.
//!
//!   magpie genkey            生成一对密钥 (私钥 + 公钥)
//!   magpie pubkey <私钥>     由私钥推导公钥
//!   magpie sample            生成一份示例配置 magpie.conf
//!   magpie up [配置文件]      启动隧道 (默认 magpie.conf, 需管理员权限)

mod config;
mod server;
mod tunnel;

use base64::{engine::general_purpose::STANDARD, Engine};
use boringtun::noise::{Tunn, TunnResult};
use boringtun::x25519::{PublicKey, StaticSecret};

const BANNER: &str = r#"
   ╔═══════════════════════════════════════╗
   ║   鹊桥 Magpie · WireGuard VPN client   ║
   ╚═══════════════════════════════════════╝
"#;

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("help");

    let result = match cmd {
        "genkey" => cmd_genkey(),
        "pubkey" => cmd_pubkey(args.get(2)),
        "sample" => cmd_sample(),
        "selftest" => cmd_selftest(),
        "testserver" => (|| -> Result<(), String> {
            let usage = "用法: magpie testserver <服务器私钥> <客户端公钥> [监听地址]";
            let sp = args.get(2).ok_or(usage)?;
            let cp = args.get(3).ok_or(usage)?;
            let listen = args.get(4).map(|s| s.as_str()).unwrap_or("0.0.0.0:51820");
            server::run(sp, cp, listen)
        })(),
        "up" => cmd_up(args.get(2).map(|s| s.as_str()).unwrap_or("magpie.conf")),
        "help" | "-h" | "--help" => {
            print_help();
            Ok(())
        }
        other => Err(format!("未知命令: `{other}`,运行 `magpie help` 查看用法")),
    };

    if let Err(e) = result {
        eprintln!("\n[错误] {e}");
        std::process::exit(1);
    }
}

fn print_help() {
    print!("{BANNER}");
    println!(
        "用法:
  magpie genkey            生成一对密钥 (私钥 + 公钥)
  magpie pubkey <私钥>     由私钥推导公钥
  magpie sample            生成示例配置 magpie.conf 及一对新密钥
  magpie up [配置文件]      启动隧道 (默认 magpie.conf, 需以管理员身份运行)
  magpie selftest          本机自检: 内存里跑通一次握手+加解密 (无需服务器/管理员)
  magpie help              显示本帮助

说明:
  · 这是一个标准 WireGuard 客户端,可连接任何标准 WireGuard 服务器。
  · 配置文件就是标准的 wg .conf 格式,能直接用别处导出的配置。
  · Windows 上 `up` 需要管理员权限,且 wintun.dll 要与 magpie.exe 同目录。"
    );
}

fn gen_keypair() -> (StaticSecret, PublicKey) {
    let mut k = [0u8; 32];
    getrandom::getrandom(&mut k).expect("系统随机源不可用");
    let secret = StaticSecret::from(k);
    let public = PublicKey::from(&secret);
    (secret, public)
}

fn cmd_genkey() -> Result<(), String> {
    let (secret, public) = gen_keypair();
    println!("PrivateKey = {}", STANDARD.encode(secret.to_bytes()));
    println!("PublicKey  = {}", STANDARD.encode(public.as_bytes()));
    Ok(())
}

fn cmd_pubkey(arg: Option<&String>) -> Result<(), String> {
    let priv_b64 = arg.ok_or("用法: magpie pubkey <base64私钥>")?;
    let bytes = STANDARD
        .decode(priv_b64.trim())
        .map_err(|e| format!("私钥 base64 解码失败: {e}"))?;
    if bytes.len() != 32 {
        return Err(format!("私钥长度 {} 字节,应为 32 字节", bytes.len()));
    }
    let mut k = [0u8; 32];
    k.copy_from_slice(&bytes);
    let secret = StaticSecret::from(k);
    let public = PublicKey::from(&secret);
    println!("{}", STANDARD.encode(public.as_bytes()));
    Ok(())
}

fn cmd_sample() -> Result<(), String> {
    let path = "magpie.conf";
    if std::path::Path::new(path).exists() {
        return Err(format!("{path} 已存在,先删除或改名以免覆盖。"));
    }
    let (secret, public) = gen_keypair();
    let conf = format!(
        "# 鹊桥 Magpie 示例配置 (标准 WireGuard 格式)
# 本机这对密钥已为你生成;把下面的【公钥】交给服务器管理员登记:
#   你的公钥: {pub}
#
# 然后把服务器给你的信息填到 [Peer] 里,再运行: magpie up

[Interface]
PrivateKey = {priv}
Address    = 10.7.0.2/24          # 服务器分配给你的内网 IP
DNS        = 1.1.1.1              # 可选
MTU        = 1420

[Peer]
PublicKey           = <把服务器公钥填到这里>
# PresharedKey      = <可选,若服务器启用了 PSK>
Endpoint            = vpn.example.com:51820   # 服务器地址:端口
AllowedIPs          = 10.7.0.0/24             # 只走内网网段=分流; 改成 0.0.0.0/0=全局
PersistentKeepalive = 25
",
        pub = STANDARD.encode(public.as_bytes()),
        priv = STANDARD.encode(secret.to_bytes()),
    );
    std::fs::write(path, conf).map_err(|e| format!("写入 {path} 失败: {e}"))?;
    println!("已生成示例配置: {path}");
    println!("你的公钥 (交给服务器管理员): {}", STANDARD.encode(public.as_bytes()));
    println!("\n编辑 {path} 填好 [Peer] 后,运行: magpie up");
    Ok(())
}

/// In-memory end-to-end test of the WireGuard core: stand up two peers
/// (a "server" and a "client"), perform the Noise handshake, then push a
/// packet through and confirm it comes out the other side byte-identical.
/// No network, no admin, no real server required.
fn cmd_selftest() -> Result<(), String> {
    print!("{BANNER}");
    println!("自检: 在内存中建立 客户端 ⇄ 服务器 两端并跑通隧道...\n");

    let (srv_priv, srv_pub) = gen_keypair();
    let (cli_priv, cli_pub) = gen_keypair();

    let mut client = Tunn::new(cli_priv, srv_pub, None, None, 0, None)
        .map_err(|e| format!("client Tunn: {e}"))?;
    let mut server = Tunn::new(srv_priv, cli_pub, None, None, 1, None)
        .map_err(|e| format!("server Tunn: {e}"))?;

    let mut a = vec![0u8; 65536]; // scratch buffers
    let mut b = vec![0u8; 65536];

    // 1) client -> handshake initiation -> server
    let init = match client.format_handshake_initiation(&mut a, false) {
        TunnResult::WriteToNetwork(p) => p.to_vec(),
        _ => return Err("客户端未能生成握手发起包".into()),
    };
    println!("  [1] 客户端发出握手发起包 ({} 字节)", init.len());

    // 2) server processes initiation -> handshake response -> client
    let resp = match server.decapsulate(None, &init, &mut b) {
        TunnResult::WriteToNetwork(p) => p.to_vec(),
        _ => return Err("服务器未能生成握手响应包".into()),
    };
    println!("  [2] 服务器回应握手响应包 ({} 字节)", resp.len());

    // 3) client processes response -> session established
    match client.decapsulate(None, &resp, &mut a) {
        TunnResult::WriteToNetwork(_) | TunnResult::Done => {}
        TunnResult::Err(e) => return Err(format!("客户端处理握手响应失败: {e:?}")),
        _ => {}
    }
    if client.time_since_last_handshake().is_none() {
        return Err("握手未完成".into());
    }
    println!("  [3] 握手完成,会话密钥已协商 ✓");

    // 4) push a fake IPv4 packet through the encrypted tunnel
    let payload = b"hello over the magpie bridge -- \xe9\xb9\x8a\xe6\xa1\xa5";
    let mut pkt = vec![0x45u8, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 10, 7, 0, 2, 10, 7, 0, 1];
    pkt.extend_from_slice(payload);
    let total = (pkt.len() as u16).to_be_bytes();
    pkt[2] = total[0];
    pkt[3] = total[1];

    let cipher = match client.encapsulate(&pkt, &mut a) {
        TunnResult::WriteToNetwork(p) => p.to_vec(),
        other => return Err(format!("加密数据包失败: {other:?}")),
    };
    println!(
        "  [4] 客户端加密 {} 字节明文 -> {} 字节密文",
        pkt.len(),
        cipher.len()
    );

    let recovered = match server.decapsulate(None, &cipher, &mut b) {
        TunnResult::WriteToTunnelV4(p, _) => p.to_vec(),
        other => return Err(format!("服务器解密失败: {other:?}")),
    };
    println!("  [5] 服务器解密还原 {} 字节", recovered.len());

    if recovered == pkt {
        println!("\n✓ 自检通过:原始报文经加密隧道往返后逐字节一致。");
        println!("  说明 WireGuard 加密内核工作正常,可以连接真实服务器了。");
        Ok(())
    } else {
        Err("还原后的数据与原始数据不一致!".into())
    }
}

fn cmd_up(path: &str) -> Result<(), String> {
    print!("{BANNER}");
    let text = std::fs::read_to_string(path)
        .map_err(|e| format!("读取配置 {path} 失败: {e}\n  先运行 `magpie sample` 生成模板。"))?;
    let cfg = config::parse(&text).map_err(|e| format!("配置 {path} 解析失败: {e}"))?;

    println!("● 配置加载成功: {path}");
    println!("    本机地址 : {}", join_nets(&cfg.addresses));
    println!("    对端      : {}", cfg.endpoint_host);
    println!("    AllowedIPs: {}", join_nets(&cfg.allowed_ips));
    println!();

    tunnel::run(cfg)
}

fn join_nets(nets: &[ipnet::IpNet]) -> String {
    nets.iter()
        .map(|n| n.to_string())
        .collect::<Vec<_>>()
        .join(", ")
}