nixpkgs/nixos/tests/sing-box.nix

549 lines
16 KiB
Nix

import ./make-test-python.nix (
{ lib, pkgs, ... }:
let
wg-keys = import ./wireguard/snakeoil-keys.nix;
target_host = "acme.test";
server_host = "sing-box.test";
hosts = {
"${target_host}" = "1.1.1.1";
"${server_host}" = "1.1.1.2";
};
hostsEntries = lib.mapAttrs' (k: v: {
name = v;
value = lib.singleton k;
}) hosts;
vmessPort = 1080;
vmessUUID = "bf000d23-0752-40b4-affe-68f7707a9661";
vmessInbound = {
type = "vmess";
tag = "inbound:vmess";
listen = "0.0.0.0";
listen_port = vmessPort;
users = [
{
name = "sekai";
uuid = vmessUUID;
alterId = 0;
}
];
};
vmessOutbound = {
type = "vmess";
tag = "outbound:vmess";
server = server_host;
server_port = vmessPort;
uuid = vmessUUID;
security = "auto";
alter_id = 0;
};
tunInbound = {
type = "tun";
tag = "inbound:tun";
interface_name = "tun0";
address = [
"172.16.0.1/30"
"fd00::1/126"
];
auto_route = true;
iproute2_table_index = 2024;
iproute2_rule_index = 9001;
route_address = [
"${hosts."${target_host}"}/32"
];
route_exclude_address = [
"${hosts."${server_host}"}/32"
];
strict_route = false;
sniff = true;
sniff_override_destination = false;
};
tproxyPort = 1081;
tproxyPost = pkgs.writeShellApplication {
name = "exe";
runtimeInputs = with pkgs; [
iproute2
iptables
];
text = ''
ip route add local default dev lo table 100
ip rule add fwmark 1 table 100
iptables -t mangle -N SING_BOX
iptables -t mangle -A SING_BOX -d 100.64.0.0/10 -j RETURN
iptables -t mangle -A SING_BOX -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A SING_BOX -d 169.254.0.0/16 -j RETURN
iptables -t mangle -A SING_BOX -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A SING_BOX -d 192.0.0.0/24 -j RETURN
iptables -t mangle -A SING_BOX -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A SING_BOX -d 240.0.0.0/4 -j RETURN
iptables -t mangle -A SING_BOX -d 255.255.255.255/32 -j RETURN
iptables -t mangle -A SING_BOX -d ${hosts."${server_host}"}/32 -p tcp -j RETURN
iptables -t mangle -A SING_BOX -d ${hosts."${server_host}"}/32 -p udp -j RETURN
iptables -t mangle -A SING_BOX -d ${hosts."${target_host}"}/32 -p tcp -j TPROXY --on-port ${toString tproxyPort} --tproxy-mark 1
iptables -t mangle -A SING_BOX -d ${hosts."${target_host}"}/32 -p udp -j TPROXY --on-port ${toString tproxyPort} --tproxy-mark 1
iptables -t mangle -A PREROUTING -j SING_BOX
iptables -t mangle -N SING_BOX_SELF
iptables -t mangle -A SING_BOX_SELF -d 100.64.0.0/10 -j RETURN
iptables -t mangle -A SING_BOX_SELF -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A SING_BOX_SELF -d 169.254.0.0/16 -j RETURN
iptables -t mangle -A SING_BOX_SELF -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A SING_BOX_SELF -d 192.0.0.0/24 -j RETURN
iptables -t mangle -A SING_BOX_SELF -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A SING_BOX_SELF -d 240.0.0.0/4 -j RETURN
iptables -t mangle -A SING_BOX_SELF -d 255.255.255.255/32 -j RETURN
iptables -t mangle -A SING_BOX_SELF -j RETURN -m mark --mark 1234
iptables -t mangle -A SING_BOX_SELF -d ${hosts."${server_host}"}/32 -p tcp -j RETURN
iptables -t mangle -A SING_BOX_SELF -d ${hosts."${server_host}"}/32 -p udp -j RETURN
iptables -t mangle -A SING_BOX_SELF -p tcp -j MARK --set-mark 1
iptables -t mangle -A SING_BOX_SELF -p udp -j MARK --set-mark 1
iptables -t mangle -A OUTPUT -j SING_BOX_SELF
'';
};
in
{
name = "sing-box";
meta = {
maintainers = with lib.maintainers; [ nickcao ];
};
nodes = {
target =
{ pkgs, ... }:
{
networking = {
firewall.enable = false;
hosts = hostsEntries;
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = [
{
address = hosts."${target_host}";
prefixLength = 24;
}
];
};
};
services.dnsmasq.enable = true;
services.nginx = {
enable = true;
package = pkgs.nginxQuic;
virtualHosts."${target_host}" = {
onlySSL = true;
sslCertificate = ./common/acme/server/acme.test.cert.pem;
sslCertificateKey = ./common/acme/server/acme.test.key.pem;
http2 = true;
http3 = true;
http3_hq = false;
quic = true;
reuseport = true;
locations."/" = {
extraConfig = ''
default_type text/plain;
return 200 "$server_protocol $remote_addr";
allow ${hosts."${server_host}"}/32;
deny all;
'';
};
};
};
};
server =
{ pkgs, ... }:
{
boot.kernel.sysctl = {
"net.ipv4.conf.all.forwarding" = 1;
};
networking = {
firewall.enable = false;
hosts = hostsEntries;
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = [
{
address = hosts."${server_host}";
prefixLength = 24;
}
];
};
};
systemd.network.wait-online.ignoredInterfaces = [ "wg0" ];
networking.wg-quick.interfaces.wg0 = {
address = [
"10.23.42.1/24"
];
listenPort = 2408;
mtu = 1500;
inherit (wg-keys.peer0) privateKey;
peers = lib.singleton {
allowedIPs = [
"10.23.42.2/32"
];
inherit (wg-keys.peer1) publicKey;
};
postUp = ''
${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -j ACCEPT
${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s 10.23.42.0/24 -o eth1 -j MASQUERADE
'';
};
services.sing-box = {
enable = true;
settings = {
inbounds = [
vmessInbound
];
outbounds = [
{
type = "direct";
tag = "outbound:direct";
}
];
};
};
};
tun =
{ pkgs, ... }:
{
networking = {
firewall.enable = false;
hosts = hostsEntries;
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = [
{
address = "1.1.1.3";
prefixLength = 24;
}
];
};
};
security.pki.certificates = [
(builtins.readFile ./common/acme/server/ca.cert.pem)
];
environment.systemPackages = [
pkgs.curlHTTP3
pkgs.iproute2
];
services.sing-box = {
enable = true;
settings = {
inbounds = [
tunInbound
];
outbounds = [
{
type = "block";
tag = "outbound:block";
}
{
type = "direct";
tag = "outbound:direct";
}
vmessOutbound
];
route = {
final = "outbound:block";
rules = [
{
inbound = [
"inbound:tun"
];
outbound = "outbound:vmess";
}
];
};
};
};
};
wireguard =
{ pkgs, ... }:
{
networking = {
firewall.enable = false;
hosts = hostsEntries;
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = [
{
address = "1.1.1.4";
prefixLength = 24;
}
];
};
};
security.pki.certificates = [
(builtins.readFile ./common/acme/server/ca.cert.pem)
];
environment.systemPackages = [
pkgs.curlHTTP3
pkgs.iproute2
];
services.sing-box = {
enable = true;
settings = {
outbounds = [
{
type = "block";
tag = "outbound:block";
}
{
type = "direct";
tag = "outbound:direct";
}
{
detour = "outbound:direct";
type = "wireguard";
tag = "outbound:wireguard";
interface_name = "wg0";
local_address = [ "10.23.42.2/32" ];
mtu = 1280;
private_key = wg-keys.peer1.privateKey;
peer_public_key = wg-keys.peer0.publicKey;
server = server_host;
server_port = 2408;
system_interface = true;
}
];
route = {
final = "outbound:block";
};
};
};
};
tproxy =
{ pkgs, ... }:
{
networking = {
firewall.enable = false;
hosts = hostsEntries;
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = [
{
address = "1.1.1.5";
prefixLength = 24;
}
];
};
};
security.pki.certificates = [
(builtins.readFile ./common/acme/server/ca.cert.pem)
];
environment.systemPackages = [ pkgs.curlHTTP3 ];
systemd.services.sing-box.serviceConfig.ExecStartPost = [
"+${tproxyPost}/bin/exe"
];
services.sing-box = {
enable = true;
settings = {
inbounds = [
{
tag = "inbound:tproxy";
type = "tproxy";
listen = "0.0.0.0";
listen_port = tproxyPort;
udp_fragment = true;
sniff = true;
sniff_override_destination = false;
}
];
outbounds = [
{
type = "block";
tag = "outbound:block";
}
{
type = "direct";
tag = "outbound:direct";
}
vmessOutbound
];
route = {
final = "outbound:block";
rules = [
{
inbound = [
"inbound:tproxy"
];
outbound = "outbound:vmess";
}
];
};
};
};
};
fakeip =
{ pkgs, ... }:
{
networking = {
firewall.enable = false;
hosts = hostsEntries;
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = [
{
address = "1.1.1.6";
prefixLength = 24;
}
];
};
};
environment.systemPackages = [ pkgs.dnsutils ];
services.sing-box = {
enable = true;
settings = {
dns = {
final = "dns:default";
independent_cache = true;
fakeip = {
enabled = true;
"inet4_range" = "198.18.0.0/16";
};
servers = [
{
detour = "outbound:direct";
tag = "dns:default";
address = hosts."${target_host}";
}
{
tag = "dns:fakeip";
address = "fakeip";
}
];
rules = [
{
outbound = [ "any" ];
server = "dns:default";
}
{
query_type = [
"A"
"AAAA"
];
server = "dns:fakeip";
}
];
};
inbounds = [
tunInbound
];
outbounds = [
{
type = "block";
tag = "outbound:block";
}
{
type = "direct";
tag = "outbound:direct";
}
{
type = "dns";
tag = "outbound:dns";
}
];
route = {
final = "outbound:direct";
rules = [
{
protocol = "dns";
outbound = "outbound:dns";
}
];
};
};
};
};
};
testScript = ''
target.wait_for_unit("nginx.service")
target.wait_for_open_port(443)
target.wait_for_unit("dnsmasq.service")
target.wait_for_open_port(53)
server.wait_for_unit("sing-box.service")
server.wait_for_open_port(1080)
server.wait_for_unit("wg-quick-wg0.service")
server.wait_for_file("/sys/class/net/wg0")
def test_curl(machine, extra_args=""):
assert (
machine.succeed(f"curl --fail --max-time 10 --http2 https://${target_host} {extra_args}")
== "HTTP/2.0 ${hosts.${server_host}}"
)
assert (
machine.succeed(f"curl --fail --max-time 10 --http3-only https://${target_host} {extra_args}")
== "HTTP/3.0 ${hosts.${server_host}}"
)
with subtest("tun"):
tun.wait_for_unit("sing-box.service")
tun.wait_for_unit("sys-devices-virtual-net-${tunInbound.interface_name}.device")
tun.wait_until_succeeds("ip route get ${hosts."${target_host}"} | grep 'dev ${tunInbound.interface_name}'")
tun.succeed("ip addr show ${tunInbound.interface_name}")
tun.succeed("ip route show table ${toString tunInbound.iproute2_table_index} | grep ${tunInbound.interface_name}")
assert (
tun.succeed("ip rule list table ${toString tunInbound.iproute2_table_index} | sort | head -1 | awk -F: '{print $1}' | tr -d '\n'")
== "${toString tunInbound.iproute2_rule_index}"
)
test_curl(tun)
with subtest("wireguard"):
wireguard.wait_for_unit("sing-box.service")
wireguard.wait_for_unit("sys-devices-virtual-net-wg0.device")
wireguard.succeed("ip addr show wg0")
test_curl(wireguard, "--interface wg0")
with subtest("tproxy"):
tproxy.wait_for_unit("sing-box.service")
test_curl(tproxy)
with subtest("fakeip"):
fakeip.wait_for_unit("sing-box.service")
fakeip.wait_for_unit("sys-devices-virtual-net-${tunInbound.interface_name}.device")
fakeip.wait_until_succeeds("ip route get ${hosts."${target_host}"} | grep 'dev ${tunInbound.interface_name}'")
fakeip.succeed("dig +short A ${target_host} @${target_host} | grep '^198.18.'")
'';
}
)