Compare commits
5 commits
0724bd08ef
...
7fb530b64e
Author | SHA1 | Date | |
---|---|---|---|
chayleaf | 7fb530b64e | ||
chayleaf | 1934073d2c | ||
chayleaf | 5edf6c4cf6 | ||
chayleaf | 050699b851 | ||
chayleaf | 9f86fa5275 |
50
flake.lock
50
flake.lock
|
@ -113,11 +113,11 @@
|
|||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1714641030,
|
||||
"narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=",
|
||||
"lastModified": 1717285511,
|
||||
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e",
|
||||
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -205,11 +205,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1715908553,
|
||||
"narHash": "sha256-9te1GH3e4gTARupbBhzqeMDKdVDHEky3AvIGzJpnm+I=",
|
||||
"lastModified": 1717550333,
|
||||
"narHash": "sha256-QebVpP3Z0zVBTSqExNQRg3FLOi2h0bPML6urBbUPzLY=",
|
||||
"owner": "fufexan",
|
||||
"repo": "nix-gaming",
|
||||
"rev": "8bd322b708faab6e8e09d300acec3ada7443b9a3",
|
||||
"rev": "de70cdf224bd40928fe2af7fa558e1bdb7d8d619",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -225,11 +225,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1715483403,
|
||||
"narHash": "sha256-WMDuQj7J5jbpXI/X/E6FZRKgBFGcaSTvYyVxPnKE6KU=",
|
||||
"lastModified": 1717297675,
|
||||
"narHash": "sha256-43UmlS1Ifx17y93/Vc258U7bOlAAIZbu8dsGDHOIIr0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-index-database",
|
||||
"rev": "f9027322f48b427da23746aa359a6510dfcd0228",
|
||||
"rev": "972a52bee3991ae1f1899e6452e0d7c01ee566d9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -240,11 +240,11 @@
|
|||
},
|
||||
"nixos-hardware": {
|
||||
"locked": {
|
||||
"lastModified": 1715881912,
|
||||
"narHash": "sha256-e4LJk5uV1wvrRkffGFZekPWvFUx29NnnOahBlLaq8Ek=",
|
||||
"lastModified": 1717574423,
|
||||
"narHash": "sha256-cz3P5MZffAHwL2IQaNzsqUBsJS+u0J/AAwArHMAcCa0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "ff1be1e3cdf884df0935ab28745ab13c3c26d828",
|
||||
"rev": "d6c6cf6f5fead4057d8fb2d5f30aa8ac1727f177",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -263,11 +263,11 @@
|
|||
"utils": "utils"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1714720456,
|
||||
"narHash": "sha256-e0WFe1BHqX23ADpGBc4ZRu38Mg+GICCZCqyS6EWCbHc=",
|
||||
"lastModified": 1717515088,
|
||||
"narHash": "sha256-nWOLpPA7+k7V1OjXTuxdsVd5jeeI0b13Di57wvnqkic=",
|
||||
"owner": "simple-nixos-mailserver",
|
||||
"repo": "nixos-mailserver",
|
||||
"rev": "41059fc548088e49e3ddb3a2b4faeb5de018e60f",
|
||||
"rev": "0d51a32e4799d081f260eb4db37145f5f4ee7456",
|
||||
"type": "gitlab"
|
||||
},
|
||||
"original": {
|
||||
|
@ -314,14 +314,14 @@
|
|||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1714640452,
|
||||
"narHash": "sha256-QBx10+k6JWz6u7VsohfSw8g8hjdBZEf8CFzXH1/1Z94=",
|
||||
"lastModified": 1717284937,
|
||||
"narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz"
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz"
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
|
||||
}
|
||||
},
|
||||
"notlua": {
|
||||
|
@ -366,11 +366,11 @@
|
|||
},
|
||||
"nur": {
|
||||
"locked": {
|
||||
"lastModified": 1715946360,
|
||||
"narHash": "sha256-abzd4TBwow7x2Se/TCIHlSF+7k7N9dEJCurUv7FrKoY=",
|
||||
"lastModified": 1717590062,
|
||||
"narHash": "sha256-Xw9yQ3KttiV/t9ClwC2Eo6EX1IEEONw4jCe9s794zhg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "NUR",
|
||||
"rev": "6572df0e6656b9f1f388c7051e070dc962d85993",
|
||||
"rev": "a364b6de0f2bd5ed5614ee3827f2ad135c69c73d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -406,11 +406,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1715912155,
|
||||
"narHash": "sha256-UXHk4dKvvm5mSuDDON3lXU5CHKiTRnIjA5mUtDOtKEU=",
|
||||
"lastModified": 1717553884,
|
||||
"narHash": "sha256-+t3XaYEvlMo5BUJ/6C6RZcEfBTWFVUdMHpNoqUU+pSE=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "d3a96b08a7280a5753246129b462eed3662815d5",
|
||||
"rev": "8795c817dfab19243a33387a16c98d2df4075bb3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -22,24 +22,24 @@
|
|||
"pinned": false,
|
||||
"src": {
|
||||
"name": null,
|
||||
"sha256": "sha256-lEoHakxH7vvUmnBVAfJJYZ87WIeS1eIz1CFMczrNVTA=",
|
||||
"sha256": "sha256-gHNrozbSBlTFY5VzRyhdiv3YkS+rXTekRJWSXlMalLY=",
|
||||
"type": "url",
|
||||
"url": "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton9-5/GE-Proton9-5.tar.gz"
|
||||
"url": "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton9-6/GE-Proton9-6.tar.gz"
|
||||
},
|
||||
"version": "GE-Proton9-5"
|
||||
"version": "GE-Proton9-6"
|
||||
},
|
||||
"searxng": {
|
||||
"cargoLocks": null,
|
||||
"date": "2024-05-17",
|
||||
"date": "2024-05-31",
|
||||
"extract": null,
|
||||
"name": "searxng",
|
||||
"passthru": null,
|
||||
"pinned": false,
|
||||
"src": {
|
||||
"sha256": "sha256-Au2XNJUfhcVd1vOzJPDTRa23cYa1SOxYGxqTM22fb80=",
|
||||
"sha256": "sha256-okE/Uxl7YqcM99kLJ4KAlMQi50x5m0bPfYp5bv62WEw=",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/searxng/searxng/archive/3585d71f99d37b58fff4c6238e95cf1fd3391898.tar.gz"
|
||||
"url": "https://github.com/searxng/searxng/archive/18fb701be225560b3fb1011cc533f785823f26a4.tar.gz"
|
||||
},
|
||||
"version": "3585d71f99d37b58fff4c6238e95cf1fd3391898"
|
||||
"version": "18fb701be225560b3fb1011cc533f785823f26a4"
|
||||
}
|
||||
}
|
|
@ -12,19 +12,19 @@
|
|||
};
|
||||
proton-ge = {
|
||||
pname = "proton-ge";
|
||||
version = "GE-Proton9-5";
|
||||
version = "GE-Proton9-6";
|
||||
src = fetchurl {
|
||||
url = "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton9-5/GE-Proton9-5.tar.gz";
|
||||
sha256 = "sha256-lEoHakxH7vvUmnBVAfJJYZ87WIeS1eIz1CFMczrNVTA=";
|
||||
url = "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton9-6/GE-Proton9-6.tar.gz";
|
||||
sha256 = "sha256-gHNrozbSBlTFY5VzRyhdiv3YkS+rXTekRJWSXlMalLY=";
|
||||
};
|
||||
};
|
||||
searxng = {
|
||||
pname = "searxng";
|
||||
version = "3585d71f99d37b58fff4c6238e95cf1fd3391898";
|
||||
version = "18fb701be225560b3fb1011cc533f785823f26a4";
|
||||
src = fetchTarball {
|
||||
url = "https://github.com/searxng/searxng/archive/3585d71f99d37b58fff4c6238e95cf1fd3391898.tar.gz";
|
||||
sha256 = "sha256-Au2XNJUfhcVd1vOzJPDTRa23cYa1SOxYGxqTM22fb80=";
|
||||
url = "https://github.com/searxng/searxng/archive/18fb701be225560b3fb1011cc533f785823f26a4.tar.gz";
|
||||
sha256 = "sha256-okE/Uxl7YqcM99kLJ4KAlMQi50x5m0bPfYp5bv62WEw=";
|
||||
};
|
||||
date = "2024-05-17";
|
||||
date = "2024-05-31";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@
|
|||
};
|
||||
"rikaitan" = buildFirefoxXpiAddon {
|
||||
pname = "rikaitan";
|
||||
version = "24.4.29.0";
|
||||
version = "24.5.21.0";
|
||||
addonId = "tatsu@autistici.org";
|
||||
url = "https://addons.mozilla.org/firefox/downloads/file/4279894/rikaitan-24.4.29.0.xpi";
|
||||
sha256 = "81c95b10e77126c29e6a5fc829b73bdc4659bd84bdefd7003388616b7912084c";
|
||||
url = "https://addons.mozilla.org/firefox/downloads/file/4291845/rikaitan-24.5.21.0.xpi";
|
||||
sha256 = "a2a94d88af04023f14daaafda1f6ca7a7197f2ab92ada08ee2e9e4292d57a391";
|
||||
meta = with lib;
|
||||
{
|
||||
homepage = "https://github.com/Ajatt-Tools/rikaitan";
|
||||
|
|
|
@ -49,7 +49,12 @@ let
|
|||
# vpn table, assign an id but don't actually add a rule for it, so it is the default
|
||||
vpn_table = 2;
|
||||
|
||||
vpn_mtu = config.networking.wireguard.interfaces.wg0.mtu;
|
||||
vpn_iface =
|
||||
if cfg.vpn.openvpn.enable && !cfg.vpn.wireguard.enable then "tun0"
|
||||
else if cfg.vpn.wireguard.enable && !cfg.vpn.openvpn.enable then "wg0"
|
||||
else throw "Exactly one of OpenVPN/Wireguard must be used";
|
||||
|
||||
vpn_mtu = config.networking.wireguard.interfaces.${vpn_iface}.mtu or 1320;
|
||||
vpn_ipv4_mss = vpn_mtu - 40;
|
||||
vpn_ipv6_mss = vpn_mtu - 60;
|
||||
|
||||
|
@ -411,7 +416,8 @@ in {
|
|||
# ethernet wan
|
||||
router.interfaces.wan = {
|
||||
dependentServices = [
|
||||
{ service = "wireguard-wg0"; inNetns = false; }
|
||||
(lib.mkIf cfg.vpn.wireguard.enable { service = "wireguard-${vpn_iface}"; inNetns = false; })
|
||||
(lib.mkIf cfg.vpn.openvpn.enable { service = "openvpn-client"; inNetns = false; })
|
||||
{ service = "wireguard-wg1"; inNetns = false; }
|
||||
];
|
||||
systemdLink.linkConfig.MACAddressPolicy = "none";
|
||||
|
@ -503,12 +509,12 @@ in {
|
|||
selfIp4 = netAddresses.lan4;
|
||||
selfIp6 = netAddresses.lan6;
|
||||
lans = [ "br0" "wg1" ];
|
||||
wans = [ "wg0" "veth-wan-a" ];
|
||||
wans = [ vpn_iface "veth-wan-a" ];
|
||||
logPrefix = "lan ";
|
||||
netdevIngressWanRules = with notnft.dsl; with payload; [
|
||||
# check oif only from vpn
|
||||
# dont check it from veth-wan-a because of dnat fuckery and because we already check packets coming from wan there
|
||||
[(is.eq meta.iifname (set [ "wg0" "wg1" ])) (is.eq (fib (f: with f; [ saddr mark iif ]) (f: f.oif)) missing) (log "lan oif missing ") drop]
|
||||
[(is.eq meta.iifname (set [ vpn_iface "wg1" ])) (is.eq (fib (f: with f; [ saddr mark iif ]) (f: f.oif)) missing) (log "lan oif missing ") drop]
|
||||
];
|
||||
inetDnatRules =
|
||||
builtins.concatLists (map
|
||||
|
@ -517,10 +523,10 @@ in {
|
|||
rule4 = rule.target4; rule6 = rule.target6;
|
||||
in with notnft.dsl; with payload;
|
||||
lib.optional (rule4 != null)
|
||||
[ (is.eq meta.iifname "wg0") (is.eq ip.protocol protocols) (is.eq th.dport rule.port)
|
||||
[ (is.eq meta.iifname vpn_iface) (is.eq ip.protocol protocols) (is.eq th.dport rule.port)
|
||||
(if rule4.port == null then dnat.ip rule4.address else dnat.ip rule4.address rule4.port) ]
|
||||
++ lib.optional (rule6 != null)
|
||||
[ (is.eq meta.iifname "wg0") (is.eq ip6.nexthdr protocols) (is.eq th.dport rule.port)
|
||||
[ (is.eq meta.iifname vpn_iface) (is.eq ip6.nexthdr protocols) (is.eq th.dport rule.port)
|
||||
(if rule6.port == null then dnat.ip6 rule6.address else dnat.ip6 rule6.address rule6.port) ]
|
||||
)
|
||||
(builtins.filter (x: x.inVpn && (x.tcp || x.udp)) cfg.dnatRules))
|
||||
|
@ -574,13 +580,14 @@ in {
|
|||
[(is.eq ip6.daddr "@block6") (log "block6 ") drop]
|
||||
[(is.eq ip.saddr "@block4") (log "block4/s ") drop]
|
||||
[(is.eq ip6.saddr "@block6") (log "block6/s ") drop]
|
||||
[(mangle meta.mark wan_table)]
|
||||
# default to vpn...
|
||||
[(mangle meta.mark vpn_table)]
|
||||
# [(mangle meta.mark vpn_table)]
|
||||
# ...but unvpn traffic to/from force_unvpn4/force_unvpn6
|
||||
[(is.eq ip.daddr "@force_unvpn4") (mangle meta.mark wan_table)]
|
||||
[(is.eq ip6.daddr "@force_unvpn6") (mangle meta.mark wan_table)]
|
||||
[(is.eq ip.saddr "@force_unvpn4") (mangle meta.mark wan_table)]
|
||||
[(is.eq ip6.saddr "@force_unvpn6") (mangle meta.mark wan_table)]
|
||||
# [(is.eq ip.daddr "@force_unvpn4") (mangle meta.mark wan_table)]
|
||||
# [(is.eq ip6.daddr "@force_unvpn6") (mangle meta.mark wan_table)]
|
||||
# [(is.eq ip.saddr "@force_unvpn4") (mangle meta.mark wan_table)]
|
||||
# [(is.eq ip6.saddr "@force_unvpn6") (mangle meta.mark wan_table)]
|
||||
# ...force vpn to/from force_vpn4/force_vpn6
|
||||
# (disable this if it breaks some sites)
|
||||
[(is.eq ip.daddr "@force_vpn4") (mangle meta.mark vpn_table)]
|
||||
|
@ -619,12 +626,12 @@ in {
|
|||
lib.optionals (rule4 != null) [
|
||||
[ (is.eq meta.iifname lanSet) (is.eq ip.protocol protocols) (is.eq ip.saddr rule4.address)
|
||||
(is.eq th.sport (if rule4.port != null then rule4.port else rule.port)) (mangle meta.mark vpn_table) ]
|
||||
[ (is.eq meta.iifname "wg0") (is.eq ip.protocol protocols) (is.eq ip.daddr rule4.address)
|
||||
[ (is.eq meta.iifname vpn_iface) (is.eq ip.protocol protocols) (is.eq ip.daddr rule4.address)
|
||||
(is.eq th.dport (if rule4.port != null then rule4.port else rule.port)) (mangle meta.mark vpn_table) ]
|
||||
] ++ lib.optionals (rule6 != null) [
|
||||
[ (is.eq meta.iifname lanSet) (is.eq ip6.nexthdr protocols) (is.eq ip6.saddr rule6.address)
|
||||
(is.eq th.sport (if rule6.port != null then rule6.port else rule.port)) (mangle meta.mark vpn_table) ]
|
||||
[ (is.eq meta.iifname "wg0") (is.eq ip6.nexthdr protocols) (is.eq ip6.daddr rule6.address)
|
||||
[ (is.eq meta.iifname vpn_iface) (is.eq ip6.nexthdr protocols) (is.eq ip6.daddr rule6.address)
|
||||
(is.eq th.dport (if rule6.port != null then rule6.port else rule.port)) (mangle meta.mark vpn_table) ]
|
||||
])
|
||||
(builtins.filter (x: x.inVpn && (x.tcp || x.udp) && dnatRuleMode x == "mark") cfg.dnatRules))
|
||||
|
@ -756,9 +763,43 @@ in {
|
|||
|
||||
# vpn socket is in wan namespace, meaning traffic gets sent through the wan namespace
|
||||
# vpn interface is in default namespace, meaning it can be used in the default namespace
|
||||
networking.wireguard.interfaces.wg0 = cfg.wireguard // {
|
||||
socketNamespace = "wan";
|
||||
interfaceNamespace = "init";
|
||||
# networking.wireguard.interfaces.${vpn_iface} = cfg.vpn.wireguard.config // {
|
||||
# socketNamespace = "wan";
|
||||
# interfaceNamespace = "init";
|
||||
# };
|
||||
|
||||
systemd.services.vpn-tunnel = {
|
||||
description = "VPN Tunnel";
|
||||
wantedBy = [
|
||||
"multi-user.target"
|
||||
(lib.mkIf cfg.vpn.openvpn.enable "openvpn-client.service")
|
||||
(lib.mkIf cfg.vpn.wireguard.enable "wireguard-${vpn_iface}.service")
|
||||
];
|
||||
after = [ "network.target" "netns-wan.service" ];
|
||||
bindsTo = [ "netns-wan.service" ];
|
||||
stopIfChanged = false;
|
||||
path = [ config.programs.ssh.package ];
|
||||
script = ''
|
||||
while true; do
|
||||
${config.programs.ssh.package}/bin/ssh \
|
||||
-i /secrets/vpn/sshtunnel.key \
|
||||
-L ${netAddresses.netnsWan4}:${toString cfg.vpn.tunnel.localPort}:127.0.0.1:${toString cfg.vpn.tunnel.remotePort} \
|
||||
-p ${toString cfg.vpn.tunnel.port} \
|
||||
-N -T -v \
|
||||
sshtunnel@${cfg.vpn.tunnel.ip}
|
||||
echo "Restarting..."
|
||||
sleep 10
|
||||
done
|
||||
'';
|
||||
serviceConfig = {
|
||||
Restart = "always";
|
||||
Type = "simple";
|
||||
NetworkNamespacePath = "/var/run/netns/wan";
|
||||
};
|
||||
};
|
||||
|
||||
services.openvpn.servers = lib.mkIf cfg.vpn.openvpn.enable {
|
||||
client.config = cfg.vpn.openvpn.config;
|
||||
};
|
||||
|
||||
# use main netns's address instead of 127.0.0.1
|
||||
|
@ -847,6 +888,7 @@ in {
|
|||
wants = [ "nftables-default.service" "avahi-daemon.service" ];
|
||||
# allow it to call nft
|
||||
serviceConfig.AmbientCapabilities = [ "CAP_NET_ADMIN" ];
|
||||
serviceConfig.CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
|
||||
};
|
||||
systemd.services.update-rkn-blacklist = {
|
||||
# fetch vpn_ips.json and vpn_domains.json for unbound
|
||||
|
@ -864,7 +906,6 @@ in {
|
|||
systemd.timers.update-rkn-blacklist = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
partOf = [ "update-rkn-blacklist.service" ];
|
||||
# slightly unusual time to reduce server load
|
||||
timerConfig.OnCalendar = [ "*-*-* 00:00:00" ]; # every day
|
||||
timerConfig.RandomizedDelaySec = 43200; # execute at random time in the first 12 hours
|
||||
};
|
||||
|
|
|
@ -5,6 +5,38 @@
|
|||
|
||||
{
|
||||
options.router-settings = {
|
||||
vpn = {
|
||||
tunnel = {
|
||||
enable = lib.mkEnableOption "VPN tunnel";
|
||||
localPort = lib.mkOption {
|
||||
description = "local port";
|
||||
type = lib.types.port;
|
||||
};
|
||||
remotePort = lib.mkOption {
|
||||
description = "remote port";
|
||||
type = lib.types.port;
|
||||
};
|
||||
ip = lib.mkOption {
|
||||
description = "remote ip";
|
||||
type = router-lib.types.ipv4;
|
||||
};
|
||||
port = lib.mkOption {
|
||||
description = "SSH port";
|
||||
type = lib.types.port;
|
||||
default = 22;
|
||||
};
|
||||
};
|
||||
openvpn.enable = lib.mkEnableOption "OpenVPN";
|
||||
openvpn.config = lib.mkOption {
|
||||
description = "OpenVPN config";
|
||||
type = lib.types.lines;
|
||||
};
|
||||
wireguard.enable = lib.mkEnableOption "Wireguard";
|
||||
wireguard.config = lib.mkOption {
|
||||
description = "wireguard config";
|
||||
type = lib.types.attrs;
|
||||
};
|
||||
};
|
||||
routerMac = lib.mkOption {
|
||||
description = "router's mac address";
|
||||
type = lib.types.str;
|
||||
|
@ -82,10 +114,6 @@
|
|||
description = "wlan passphrase";
|
||||
type = lib.types.str;
|
||||
};
|
||||
wireguard = lib.mkOption {
|
||||
description = "wireguard config";
|
||||
type = lib.types.attrs;
|
||||
};
|
||||
dhcpReservations = lib.mkOption {
|
||||
description = "dhcp reservations (ipv4)";
|
||||
default = [ ];
|
||||
|
|
|
@ -5,33 +5,19 @@
|
|||
|
||||
let
|
||||
cfg = config.server;
|
||||
python = pkgs.python3.withPackages (p: with p; [ cryptography pyasn1 pyasn1-modules requests ]);
|
||||
tool = pkgs.writeScript "certspotter.py" ''
|
||||
#!${python}/bin/python3
|
||||
${builtins.readFile ./certspotter.py}
|
||||
'';
|
||||
in {
|
||||
security.acme.certs = lib.flip builtins.mapAttrs (lib.filterAttrs (k: v: v.enableACME) config.services.nginx.virtualHosts) (k: v: {
|
||||
postRun = let
|
||||
python = pkgs.python3.withPackages (p: with p; [ cryptography pyasn1 pyasn1-modules ]);
|
||||
tbs-hash = pkgs.writeScript "tbs-hash.py" ''
|
||||
#!${python}/bin/python3
|
||||
import hashlib
|
||||
from pyasn1.codec.der.decoder import decode
|
||||
from pyasn1.codec.der.encoder import encode
|
||||
from pyasn1_modules import rfc5280
|
||||
from cryptography import x509
|
||||
|
||||
with open('full.pem', 'rb') as f:
|
||||
cert = x509.load_pem_x509_certificate(f.read())
|
||||
tbs, _leftover = decode(cert.tbs_certificate_bytes, asn1Spec=rfc5280.TBSCertificate())
|
||||
precert_exts = [v.dotted_string for k, v in x509.ExtensionOID.__dict__.items() if k.startswith('PRECERT_')]
|
||||
exts = [ext for ext in tbs["extensions"] if str(ext["extnID"]) not in precert_exts]
|
||||
tbs["extensions"].clear()
|
||||
tbs["extensions"].extend(exts)
|
||||
print(hashlib.sha256(encode(tbs)).hexdigest())
|
||||
'';
|
||||
in ''
|
||||
${tbs-hash} > "/var/lib/certspotter/tbs-hashes/${k}"
|
||||
security.acme.certs = lib.mkIf config.services.certspotter.enable (lib.flip builtins.mapAttrs (lib.filterAttrs (k: v: v.enableACME) config.services.nginx.virtualHosts) (k: v: {
|
||||
postRun = ''
|
||||
${tool} tbs full.pem > "/var/lib/certspotter/tbs-hashes/${k}"
|
||||
'';
|
||||
});
|
||||
}));
|
||||
services.certspotter = {
|
||||
enable = true;
|
||||
enable = false;
|
||||
extraFlags = [ ];
|
||||
watchlist = [ ".${cfg.domainName}" ];
|
||||
hooks = lib.toList (pkgs.writeShellScript "certspotter-hook" ''
|
||||
|
@ -41,4 +27,25 @@ in {
|
|||
(echo "Subject: $SUMMARY" && echo && cat "$TEXT_FILENAME") | /run/wrappers/bin/sendmail -i webmaster-certspotter@${cfg.domainName}
|
||||
'');
|
||||
};
|
||||
systemd.services.certspotter-lite = {
|
||||
script = ''
|
||||
exec ${tool} spot \
|
||||
-c /var/lib/acme/certspotter-lite.txt \
|
||||
-d ${cfg.domainName} \
|
||||
-t webmaster-certspotter@${cfg.domainName} \
|
||||
-s /run/wrappers/bin/sendmail \
|
||||
/var/lib/acme/*/full.pem
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = "acme";
|
||||
Group = "acme";
|
||||
Type = "oneshot";
|
||||
};
|
||||
};
|
||||
systemd.timers.certspotter-lite = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
partOf = [ "certspotter-lite.service" ];
|
||||
timerConfig.OnCalendar = [ "*-*-* 00:00:00" ]; # every day
|
||||
timerConfig.RandomizedDelaySec = 43200; # execute at random time in the first 12 hours
|
||||
};
|
||||
}
|
||||
|
|
158
system/hosts/server/certspotter.py
Normal file
158
system/hosts/server/certspotter.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
import argparse
|
||||
import hashlib
|
||||
import requests
|
||||
import subprocess
|
||||
import traceback
|
||||
from datetime import date
|
||||
from pyasn1.codec.der.decoder import decode
|
||||
from pyasn1.codec.der.encoder import encode
|
||||
from pyasn1_modules import rfc5280
|
||||
from cryptography import x509
|
||||
|
||||
|
||||
def calc_tbs(pem: bytes) -> str:
|
||||
cert = x509.load_pem_x509_certificate(pem)
|
||||
tbs, _leftover = decode(
|
||||
cert.tbs_certificate_bytes, asn1Spec=rfc5280.TBSCertificate()
|
||||
)
|
||||
precert_exts = [
|
||||
v.dotted_string
|
||||
for k, v in x509.ExtensionOID.__dict__.items()
|
||||
if k.startswith("PRECERT_")
|
||||
]
|
||||
exts = [ext for ext in tbs["extensions"] if str(ext["extnID"]) not in precert_exts]
|
||||
tbs["extensions"].clear()
|
||||
tbs["extensions"].extend(exts)
|
||||
return hashlib.sha256(encode(tbs)).hexdigest()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(required=True)
|
||||
spot = subparsers.add_parser("spot")
|
||||
spot.set_defaults(func=spotter)
|
||||
spot.add_argument("--sendmail", "-s", type=str, required=True)
|
||||
spot.add_argument("--from", "-f", type=str, required=False, default="Certificate Monitoring")
|
||||
spot.add_argument("--to", "-t", type=str, required=False)
|
||||
spot.add_argument("--domain", "-d", type=str, required=True)
|
||||
spot.add_argument("--cache_file", "-c", type=str, required=True)
|
||||
spot.add_argument("certs", type=str, nargs="*")
|
||||
hash = subparsers.add_parser("hash")
|
||||
hash.set_defaults(func=print_hash)
|
||||
hash.add_argument("path", type=str)
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
def print_hash(args) -> None:
|
||||
with open(args.path, "rb") as f:
|
||||
print(calc_tbs(f.read()))
|
||||
|
||||
|
||||
def send_mail(
|
||||
sendmail: str, from_: str | None, to: str | None, subject: str, text: str
|
||||
):
|
||||
proc = subprocess.Popen(
|
||||
[sendmail, "-i"] + (["-F", from_] if from_ else []) + (['--', to] if to else []),
|
||||
stdin=subprocess.PIPE,
|
||||
)
|
||||
assert proc.stdin is not None
|
||||
proc.stdin.write(f"Subject: {subject}\n\n".encode("utf-8"))
|
||||
proc.stdin.write((text + "\n").encode("utf-8"))
|
||||
proc.stdin.close()
|
||||
proc.wait()
|
||||
assert proc.returncode == 0
|
||||
|
||||
|
||||
def spotter(args) -> None:
|
||||
try:
|
||||
spotter1(args)
|
||||
except Exception:
|
||||
subject = "Certificate monitoring failure"
|
||||
text = traceback.format_exc()
|
||||
send_mail(args.sendmail, args.__dict__["from"], args.to, subject, text)
|
||||
|
||||
|
||||
def spotter1(args) -> None:
|
||||
url = f"https://crt.sh/?CN={args.domain}&dir=^&sort=1&group=none"
|
||||
|
||||
try:
|
||||
with open(args.cache_file, "rt") as f:
|
||||
lastid = int(f.read())
|
||||
except FileNotFoundError:
|
||||
lastid = 0
|
||||
|
||||
body = requests.get(url).text
|
||||
|
||||
def parse_row(row: str, tag: str) -> list[str]:
|
||||
ret = []
|
||||
for col in row.split(f"</{tag}>")[:-1]:
|
||||
if "</A>" in col:
|
||||
col = col.split("</A>")[-2].split(">")[-1]
|
||||
else:
|
||||
col = col.split(">")[-1]
|
||||
ret.append(col)
|
||||
return ret
|
||||
|
||||
cols: list[str] = []
|
||||
rows: list[dict[str, str]] = []
|
||||
for s_row in body.split("<TR>")[2:]:
|
||||
s_row = s_row.split("</TR>")[0]
|
||||
if "<TH" in s_row:
|
||||
cols = parse_row(s_row, "TH")
|
||||
elif cols:
|
||||
rows.append({k: v for k, v in zip(cols, parse_row(s_row, "TD"))})
|
||||
|
||||
today = date.today()
|
||||
pem_urls = {}
|
||||
issuers = {}
|
||||
cns = {}
|
||||
|
||||
if not rows:
|
||||
raise Exception("No rows found!")
|
||||
|
||||
for row in rows:
|
||||
crtid = int(row["crt.sh ID"])
|
||||
if crtid <= lastid:
|
||||
continue
|
||||
d = date.fromisoformat(row["Logged At"])
|
||||
if (today - d).days > 30:
|
||||
continue
|
||||
pem_urls[crtid] = f"https://crt.sh/?d={crtid}"
|
||||
issuers[crtid] = row.get("Issuer Name", "")
|
||||
cns[crtid] = row.get("Matching Identities", "")
|
||||
|
||||
if not pem_urls:
|
||||
return
|
||||
|
||||
valid_hashes: set[str] = set()
|
||||
for path in args.certs:
|
||||
with open(path, "rb") as f1:
|
||||
valid_hashes.add(calc_tbs(f1.read()))
|
||||
|
||||
pems: dict[int, bytes] = {}
|
||||
|
||||
for id, pem_url in pem_urls.items():
|
||||
lastid = max(id, lastid)
|
||||
pems[id] = requests.get(pem_url).content
|
||||
|
||||
invalid_ids: set[int] = set()
|
||||
|
||||
for id, pem in pems.items():
|
||||
if calc_tbs(pem) not in valid_hashes:
|
||||
invalid_ids.add(id)
|
||||
|
||||
if invalid_ids:
|
||||
subject = f"{len(invalid_ids)} invalid certs discovered!"
|
||||
text = "\n".join(
|
||||
f"https://crt.sh/?id={id} ({cns[id]}, {issuers[id]})"
|
||||
for id in sorted(invalid_ids)
|
||||
)
|
||||
send_mail(args.sendmail, args.__dict__["from"], args.to, subject, text)
|
||||
|
||||
with open(args.cache_file, "wt") as f:
|
||||
f.write(str(lastid))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in a new issue