Files
clan-core/nixosModules/user-firewall/default.nix
Jörg Thalheim 5d88ac440a add restricted network nixos modules
See README.md for explanation
2025-06-27 18:44:17 +02:00

148 lines
4.6 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.networking.user-firewall;
# Get all normal users (excluding system users)
normalUsers = lib.filterAttrs (_name: user: user.isNormalUser) config.users.users;
# Get usernames for normal users who aren't exempt
restrictedUsers = lib.attrNames (
lib.filterAttrs (name: _user: !(lib.elem name cfg.exemptUsers)) normalUsers
);
# Convert interface patterns for iptables
# iptables uses + for one-or-more, but we use * in our interface
toIptablesPattern =
pattern:
if lib.hasSuffix "*" pattern && pattern != "*" then
# Convert "wg*" to "wg+" for iptables
lib.removeSuffix "*" pattern + "+"
else
pattern;
# Build interface patterns for iptables with proper escaping
interfaceRules = lib.concatMapStringsSep "\n " (
iface:
"ip46tables -A user-firewall-output -o ${lib.escapeShellArg (toIptablesPattern iface)} -j RETURN"
) cfg.allowedInterfaces;
in
{
options.networking.user-firewall = {
allowedInterfaces = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"lo" # loopback (allows Tor on localhost)
"tun*" # OpenVPN, OpenConnect
"tap*" # OpenVPN (bridged mode)
"wg*" # WireGuard (wg0, wg-home, etc.)
"tailscale*" # Tailscale
"zt*" # ZeroTier
"vpn*" # Generic VPN interfaces
"ipsec*" # IPSec
"nebula*" # Nebula
"tinc*" # Tinc
"edge*" # n2n
"hyprspace" # Hyprspace
"ham0" # Hamachi
"easytier" # EasyTier
"mycelium" # Mycelium
];
description = ''
Network interfaces that normal users can use.
Supports wildcards: * (zero or more characters).
'';
};
exemptUsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Users exempt from network restrictions.
'';
};
};
config = {
assertions = [
{
assertion = config.networking.firewall.enable;
message = "networking.user-firewall requires networking.firewall.enable to be true";
}
];
# For nftables: create fake passwd file for build-time validation
networking.nftables.checkRulesetRedirects = lib.mkIf config.networking.nftables.enable (
lib.mkOptionDefault {
"/etc/passwd" = pkgs.writeText "passwd" (
let
userList = lib.attrNames config.users.users;
indexedUsers = lib.imap0 (
i: name: "${name}:x:${toString (1000 + i)}:100::/home/${name}:/bin/sh"
) userList;
in
lib.concatStringsSep "\n" indexedUsers
);
}
);
# For iptables backend
networking.firewall.extraCommands = lib.mkIf (!config.networking.nftables.enable) ''
# Create custom chain for user firewall output
ip46tables -N user-firewall-output 2>/dev/null || true
ip46tables -F user-firewall-output
# Allow traffic on permitted interfaces
${interfaceRules}
# Reject traffic from restricted users (TCP RST for TCP, ICMP for UDP)
${lib.concatMapStringsSep "\n " (
user: "ip46tables -A user-firewall-output -m owner --uid-owner ${lib.escapeShellArg user} -j REJECT"
) restrictedUsers}
# Allow all other traffic
ip46tables -A user-firewall-output -j RETURN
# Insert our chain at the beginning of OUTPUT
ip46tables -D OUTPUT -j user-firewall-output 2>/dev/null || true
ip46tables -I OUTPUT -j user-firewall-output
'';
networking.firewall.extraStopCommands = lib.mkIf (!config.networking.nftables.enable) ''
# Remove our custom chain
ip46tables -D OUTPUT -j user-firewall-output 2>/dev/null || true
ip46tables -F user-firewall-output 2>/dev/null || true
ip46tables -X user-firewall-output 2>/dev/null || true
'';
# For nftables backend
networking.nftables.tables = lib.mkIf config.networking.nftables.enable {
user-firewall = {
family = "inet";
content = ''
chain output {
type filter hook output priority 0; policy accept;
# Allow traffic on permitted interfaces
${lib.concatMapStringsSep "\n " (
iface: ''oifname "${iface}" counter accept comment "allow ${iface}"''
) cfg.allowedInterfaces}
# Reject traffic from restricted users (TCP RST for TCP, ICMP for UDP)
${lib.concatMapStringsSep "\n " (
user: ''meta skuid "${user}" counter reject comment "blocked user ${user}"''
) restrictedUsers}
}
'';
};
};
};
}