-- wireguard_conf_test.lua — wg-quick / AmneziaWG .conf 导入单元测试 -- 用法:lua5.1 tests/wireguard_conf_test.lua package.path = "./root/usr/share/?.lua;" .. package.path local parser = require("substore.parser") local output_clash_meta = require("substore.output_clash_meta") local output_singbox = require("substore.output_singbox") local output_uri = require("substore.output_uri") local passed, failed = 0, 0 local function check(name, cond) if cond then passed = passed + 1 print("PASS " .. name) else failed = failed + 1 print("FAIL " .. name) end end -- ---------- 标准 wg-quick .conf ---------- local WG_CONF = [[ [Interface] PrivateKey = aGVsbG93b3JsZHByaXZhdGVrZXkwMDAwMDAwMDAwMDA= Address = 10.7.0.2/32, fd00:7::2/128 DNS = 1.1.1.1 MTU = 1420 ListenPort = 51820 [Peer] PublicKey = cGVlcnB1YmxpY2tleTAwMDAwMDAwMDAwMDAwMDAwMDAwMDA= PresharedKey = cHJlc2hhcmVka2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA= AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = wg.example.com:51820 PersistentKeepalive = 25 ]] check("detect wg conf", parser.detect(WG_CONF) == "wireguard-conf") local res = parser.parse(WG_CONF) check("parse wg conf ok", res ~= nil and res.format == "wireguard-conf") check("parse wg conf one node", res and #res.nodes == 1) local n = res and res.nodes[1] or {} check("conf proto", n.proto == "wireguard") check("conf server", n.server == "wg.example.com") check("conf port", n.port == 51820) check("conf private-key", n["private-key"] == "aGVsbG93b3JsZHByaXZhdGVrZXkwMDAwMDAwMDAwMDA=") check("conf public-key", n["public-key"] == "cGVlcnB1YmxpY2tleTAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=") check("conf pre-shared-key", n["pre-shared-key"] == "cHJlc2hhcmVka2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=") check("conf ip", n.ip == "10.7.0.2/32") check("conf ipv6", n.ipv6 == "fd00:7::2/128") check("conf allowed-ips array", type(n["allowed-ips"]) == "table" and #n["allowed-ips"] == 2) check("conf allowed-ips v4", n["allowed-ips"] and n["allowed-ips"][1] == "0.0.0.0/0") check("conf allowed-ips v6", n["allowed-ips"] and n["allowed-ips"][2] == "::/0") check("conf persistent-keepalive", n["persistent-keepalive"] == 25) check("conf listen-port", n["listen-port"] == 51820) check("conf mtu", n.mtu == 1420) check("conf dns single is string", n.dns == "1.1.1.1") check("conf default name", n.name == "wg.example.com:51820") -- ---------- AmneziaWG .conf(含 Jc/Jmin/Jmax/S1/S2/H1..H4) ---------- local AWG_CONF = [[ [Interface] PrivateKey = YW1uZXppYXByaXZhdGVrZXkwMDAwMDAwMDAwMDAwMDA= Address = 10.8.1.5/32 DNS = 1.1.1.1, 8.8.8.8 Jc = 5 Jmin = 50 Jmax = 1000 S1 = 86 S2 = 574 H1 = 1234567 H2 = 2345678 H3 = 3456789 H4 = 4567890 [Peer] PublicKey = YW1uZXppYXBlZXJrZXkwMDAwMDAwMDAwMDAwMDAwMDAwMDA= AllowedIPs = 0.0.0.0/0 Endpoint = 203.0.113.9:51820 ]] check("detect awg conf", parser.detect(AWG_CONF) == "wireguard-conf") local ares = parser.parse(AWG_CONF) check("parse awg conf ok", ares ~= nil and #ares.nodes == 1) local a = ares and ares.nodes[1] or {} check("awg proto", a.proto == "wireguard") check("awg server", a.server == "203.0.113.9") check("awg port", a.port == 51820) check("awg ip", a.ip == "10.8.1.5/32") check("awg dns multi is array", type(a.dns) == "table" and #a.dns == 2) check("awg dns first", type(a.dns) == "table" and a.dns[1] == "1.1.1.1") local opt = a["amnezia-wg-option"] check("awg option exists", type(opt) == "table") check("awg jc", opt and opt.jc == 5) check("awg jmin", opt and opt.jmin == 50) check("awg jmax", opt and opt.jmax == 1000) check("awg s1", opt and opt.s1 == 86) check("awg s2", opt and opt.s2 == 574) check("awg h1", opt and opt.h1 == 1234567) check("awg h2", opt and opt.h2 == 2345678) check("awg h3", opt and opt.h3 == 3456789) check("awg h4", opt and opt.h4 == 4567890) -- ---------- 大小写不敏感 / 注释 / 未知键丢弃 ---------- local LOOSE_CONF = [[ # AmneziaWG export ; another comment style [interface] privatekey = a2V5 address = 10.9.0.3/32 s3 = 12 s4 = 34 UnknownFutureKey = should-be-dropped [peer] publickey = cHVi allowedips = 0.0.0.0/0 endpoint = [2001:db8::1]:51821 ]] check("detect loose conf", parser.detect(LOOSE_CONF) == "wireguard-conf") local lres = parser.parse(LOOSE_CONF) local l = lres and lres.nodes[1] or {} check("loose private-key", l["private-key"] == "a2V5") check("loose ipv6 endpoint host", l.server == "2001:db8::1") check("loose ipv6 endpoint port", l.port == 51821) check("loose s3", l["amnezia-wg-option"] and l["amnezia-wg-option"].s3 == 12) check("loose s4", l["amnezia-wg-option"] and l["amnezia-wg-option"].s4 == 34) check("loose unknown key dropped", l["amnezia-wg-option"] and l["amnezia-wg-option"].unknownfuturekey == nil) -- ---------- 缺少 Endpoint 应失败,而不是产出半成品节点 ---------- local BAD_CONF = "[Interface]\nPrivateKey = a2V5\nAddress = 10.0.0.1/32\n" check("bad conf no endpoint", parser.detect(BAD_CONF) == "wireguard-conf") local bres, berr = parser.parse(BAD_CONF) check("bad conf returns error", bres == nil and type(berr) == "string") -- ---------- 导出:clash.meta ---------- local clash = output_clash_meta.generate({ n }) check("clash conf has private-key", clash:find("private%-key: aGVsbG93b3JsZHByaXZhdGVrZXkwMDAwMDAwMDAwMDA=") ~= nil) check("clash conf has public-key", clash:find("public%-key: cGVlcnB1YmxpY2tleTAw") ~= nil) check("clash conf has ip", clash:find("ip: 10%.7%.0%.2/32") ~= nil) -- esc_yaml 对含 ":" 的值会加引号(合法且更安全的 YAML),故断言带引号形式 check("clash conf has ipv6", clash:find('ipv6: "fd00:7::2/128"') ~= nil) check("clash conf has listen-port", clash:find("listen%-port: 51820") ~= nil) -- allowed-ips 必须是合法 YAML 列表,不能出现 table: 0x... check("clash allowed-ips not table-dump", clash:find("table: 0x") == nil) check("clash allowed-ips list", clash:find('allowed%-ips:\n%s+%- 0%.0%.0%.0/0\n%s+%- "::/0"') ~= nil) -- ---------- 导出:sing-box(local_address 必须是数组) ---------- local sb = output_singbox.generate({ n }) check("singbox local_address array", sb:find('"local_address":%["10%.7%.0%.2/32","fd00:7::2/128"%]') ~= nil) check("singbox listen_port", sb:find('"listen_port":51820') ~= nil) check("singbox allowed_ips array", sb:find('"allowed_ips":%["0%.0%.0%.0/0","::/0"%]') ~= nil) -- 只有 IPv4 时 local_address 退化为字符串 local v4only = output_singbox.generate({ { proto = "wireguard", name = "v4", server = "1.2.3.4", port = 51820, ip = "10.0.0.1/32" } }) check("singbox single local_address string", v4only:find('"local_address":"10%.0%.0%.1/32"') ~= nil) -- ---------- 导出:AmneziaWG 参数稳定排序 ---------- local awg_clash = output_clash_meta.generate({ a }) check("awg clash option block", awg_clash:find("amnezia%-wg%-option:") ~= nil) check("awg clash no table-dump", awg_clash:find("table: 0x") == nil) local i_h1 = awg_clash:find(" h1: 1234567") local i_jc = awg_clash:find(" jc: 5") local i_s1 = awg_clash:find(" s1: 86") check("awg clash sorted h1 0) -- 空字符串等于「未设置」 local empty = parser.parse_local(util.json_encode({ { type = "wireguard", server = "1.2.3.4", port = "51820", ["amnezia-wg-option"] = "", } }), "form").nodes[1] check("form: empty awg json cleared", empty["amnezia-wg-option"] == nil) end -- ---------- M9:行内注释 ---------- -- wg-quick 的 parse_options 用 `stripped="${line%%\#*}"`,即从**第一个** # 起 -- 全部丢弃(不要求 # 前有空白),所以 .conf 是支持行内注释的。旧实现只跳整行 -- 注释,`Endpoint = 1.2.3.4:51820 # 备用` 会把 "# 备用" 当成值的一部分, -- split_hostport 取不到端口 → 整份 .conf 报错。 local INLINE_CONF = [[ [Interface] PrivateKey = PRIVI # 主密钥 Address = 10.11.0.2/32 # 隧道地址 DNS = 1.1.1.1, 8.8.8.8 # 双 DNS [Peer] PublicKey = PUBI # 对端公钥 AllowedIPs = 0.0.0.0/0, ::/0 # 全量路由 Endpoint = 198.51.100.7:51820 # 备用入口 PersistentKeepalive = 25 # 保活 ]] local ires = parser.parse(INLINE_CONF, "wireguard-conf") check("inline comment conf parses", ires ~= nil and ires.nodes and #ires.nodes == 1) local inode = ires and ires.nodes[1] or {} check("inline comment endpoint host", inode.server == "198.51.100.7") check("inline comment endpoint port", inode.port == 51820) check("inline comment private-key", inode["private-key"] == "PRIVI") check("inline comment public-key", inode["public-key"] == "PUBI") check("inline comment allowed-ips", type(inode["allowed-ips"]) == "table" and #inode["allowed-ips"] == 2 and inode["allowed-ips"][2] == "::/0") check("inline comment keepalive", inode["persistent-keepalive"] == 25) check("inline comment dns array", type(inode.dns) == "table" and #inode.dns == 2) -- ---------- M9:一个坏 [Peer] 不得废掉整份文件 ---------- -- 旧实现在第一个失败的对端上直接 return nil,同文件里其它完好的对端全部丢失。 local PARTIAL_CONF = [[ [Interface] PrivateKey = PRIVP Address = 10.12.0.2/32 [Peer] PublicKey = NOENDPOINT AllowedIPs = 10.0.0.0/8 [Peer] PublicKey = GOODPEER AllowedIPs = 0.0.0.0/0 Endpoint = 203.0.113.77:51820 ]] local pres = parser.parse(PARTIAL_CONF, "wireguard-conf") check("partial conf still parses", pres ~= nil and pres.nodes ~= nil) check("partial conf keeps the good peer", pres and pres.nodes and #pres.nodes == 1) check("partial conf good peer server", pres and pres.nodes[1] and pres.nodes[1].server == "203.0.113.77") check("partial conf good peer key", pres and pres.nodes[1] and pres.nodes[1]["public-key"] == "GOODPEER") -- 全部 [Peer] 都缺 Endpoint:必须报错,不能返回空列表 —— -- 空列表会被上层当成「解析成功但 0 节点」(静默失败) local ALLBAD_CONF = [[ [Interface] PrivateKey = PRIVB [Peer] PublicKey = B1 [Peer] PublicKey = B2 Endpoint = :51820 ]] local abres, aberr = parser.parse(ALLBAD_CONF, "wireguard-conf") check("all-bad peers returns nil", abres == nil) check("all-bad peers returns error", type(aberr) == "string" and #aberr > 0) -- ---------- L20:多 [Peer] 时数组字段不得共享同一个表 ---------- -- common 里的 dns / reserved 是数组。浅拷贝让所有节点指向同一个表, -- 按节点编辑 DNS 会同时改到全部节点。 local SHARE_CONF = [[ [Interface] PrivateKey = PRIVS Address = 10.13.0.2/32 DNS = 1.1.1.1, 8.8.8.8 Reserved = 1, 2, 3 [Peer] PublicKey = S1 AllowedIPs = 0.0.0.0/0 Endpoint = 198.51.100.1:51820 [Peer] PublicKey = S2 AllowedIPs = 10.0.0.0/8 Endpoint = 198.51.100.2:51820 ]] local sres = parser.parse(SHARE_CONF, "wireguard-conf") check("shared-table conf two nodes", sres ~= nil and sres.nodes and #sres.nodes == 2) if sres and sres.nodes and #sres.nodes == 2 then local s1, s2 = sres.nodes[1], sres.nodes[2] check("dns tables distinct", type(s1.dns) == "table" and type(s2.dns) == "table" and s1.dns ~= s2.dns) check("reserved tables distinct", type(s1.reserved) == "table" and type(s2.reserved) == "table" and s1.reserved ~= s2.reserved) check("allowed-ips tables distinct", s1["allowed-ips"] ~= s2["allowed-ips"]) check("shared awg tables distinct", s1["amnezia-wg-option"] == nil and s2["amnezia-wg-option"] == nil) -- 改一个节点不能影响另一个 s1.dns[1] = "MUTATED" s1.reserved[1] = 99 check("dns mutation isolated", s2.dns[1] == "1.1.1.1") check("reserved mutation isolated", s2.reserved[1] == 1) end -- ---------- 结果 ---------- -- H7:.conf 的注释行同样要压成单行。节点名来自订阅(不可信输入),含换行时 -- 会从注释里注入出真正的配置行,伪造出一段用户没写过的隧道配置。 local wgconf_out = require("substore.output_wireguard_conf") local wg_evil = wgconf_out.generate({ { proto = "wireguard", name = "x\n[Interface]\nPrivateKey = injected", server = "1.2.3.4", port = 51820, ["private-key"] = "PRIV", ["public-key"] = "PUB" } }) -- 注入文本只能留在注释行里:既不能出现真正的 [Interface] 段头,也不能出现 -- 以 "PrivateKey = injected" 开头的新行 check("wgconf comment not injected", wg_evil:find("\n[Interface]\nPrivateKey = injected", 1, true) == nil) check("wgconf injected text stays in comment", wg_evil:find("\nPrivateKey = injected", 1, true) == nil) local real_iface = 0 for l in wg_evil:gmatch("[^\n]+") do if l == "[Interface]" then real_iface = real_iface + 1 end end check("wgconf single real Interface section", real_iface == 1) check("wgconf real private key kept", wg_evil:find("\nPrivateKey = PRIV", 1, true) ~= nil) check("wgconf comment is one line", wg_evil:match("^# [^\n]*\n") ~= nil) print(string.format("\n%d passed, %d failed", passed, failed)) os.exit(failed == 0 and 0 or 1)