{
config,
lib,
...
}:
let
inherit (lib)
imap0
attrValues
mergeAttrsList
replaceString
concatImapStringsSep
;
definition = import ./definition.nix;
inherit (definition) paths users mask;
ownAddress = "10.123.X.1"; # ip of host running the ingress vpn (per-interface)
addressFromTemplate =
index: template: prefix:
"${replaceString "X" (toString (index + 1)) template}/${toString prefix}";
ingressName = index: "tunnel-ingress${toString index}";
egressName = "tunnel-egress0";
egressAddress = "10.123.255.1/16"; # /16 encompasses all possible subnet addresses
egressHost = destination: "${destination}.tunnel.rnrd.eu"; # e.g. "finland.tunnel.rnrd.eu"
mtu = 1400;
in
{
networking.firewall = {
allowedUDPPorts = map (x: x.port) paths;
allowedTCPPorts = map (x: x.port) paths;
checkReversePath = "loose";
};
age.secrets.wg-private-key = {
file = ../../secrets/wg-private-key.age;
owner = "systemd-network";
};
systemd.network =
let
mkNetdev = index: path: {
"10-${ingressName index}" = {
netdevConfig = {
Kind = "wireguard";
Name = ingressName index;
};
wireguardConfig = {
PrivateKeyFile = config.age.secrets.wg-private-key.path;
ListenPort = path.port;
};
wireguardPeers = map (user: {
PublicKey = user.key;
AllowedIPs = [ (addressFromTemplate index user.ip 32) ];
}) (attrValues users);
};
};
mkNetwork = index: path: {
"10-${ingressName index}" = {
name = ingressName index;
address = [ (addressFromTemplate index ownAddress 24) ];
linkConfig = {
RequiredForOnline = "no";
MTUBytes = toString mtu;
};
routingPolicyRules = [
{
IncomingInterface = ingressName index;
Table = 100;
}
];
};
};
ingressNetdevs = imap0 mkNetdev paths;
ingressNetworks = imap0 mkNetwork paths;
egressNetworks = [
{
"20-${egressName}" = {
name = egressName;
address = [ egressAddress ];
networkConfig = {
IPv4ReversePathFilter = "loose";
};
linkConfig = {
ActivationPolicy = "up";
RequiredForOnline = "no";
MTUBytes = toString mtu;
};
routes = [
{
Destination = "0.0.0.0/0";
Table = 100;
Scope = "link";
}
];
};
}
];
in
{
netdevs = mergeAttrsList ingressNetdevs;
networks = mergeAttrsList (ingressNetworks ++ egressNetworks);
};
# allow forwarding packets between egress and ingress, but avoid any snat,
# ip should always keep it's origin form, for correct egress routing.
# also adapt mss to outgoing mss value, so that we don't shatter packets.
networking.nftables.tables.tunnel-forward =
let
ingressInterfaces = concatImapStringsSep "\", \"" (i: _: ingressName (i - 1)) paths;
in
{
family = "inet";
content = ''
chain forward {
type filter hook forward priority 0; policy drop;
tcp flags syn tcp option maxseg size set rt mtu
iifname { "${ingressInterfaces}" } oifname "${egressName}" accept
iifname "${egressName}" oifname { "${ingressInterfaces}" } accept
}
'';
};
# sing-box is a vpn client supporting various protocols which will allow us
# to configure it in whichever way we want to avoid russian dpi.
# in this case, our communications crossing the borders are relying on vless.
services.sing-box =
let
inboundName = "tunnel-in";
outboundName = egress: "tunnel-out-${egress}";
in
{
enable = true;
settings = {
inbounds = [
{
inherit mtu;
type = "tun";
tag = inboundName;
interface_name = egressName;
address = [ egressAddress ];
stack = "gvisor";
auto_route = false; # we route manually
strict_route = false;
endpoint_independent_nat = true;
}
];
outbounds = map (path: {
type = "vless";
flow = "xtls-rprx-vision";
server = egressHost path.destination;
server_port = 443;
tag = outboundName path.egress;
uuid = path.info.uuid;
tls = {
enabled = true;
server_name = "www.${mask}";
utls = {
enabled = true;
fingerprint = "chrome";
};
reality = {
enabled = true;
public_key = path.info.public;
short_id = path.info.short;
};
};
}) paths;
route = {
rules = imap0 (index: path: {
inbound = inboundName;
source_ip_cidr = [ (addressFromTemplate index "10.123.X.0" 24) ];
outbound = outboundName path.egress;
}) paths;
auto_detect_interface = true;
};
log = {
level = "debug";
timestamp = true;
};
};
};
}