Rewrite VPN container

This commit is contained in:
2022-05-28 18:54:41 -04:00
parent c560a63182
commit 7e615f814d
13 changed files with 132 additions and 77 deletions

View File

@@ -0,0 +1,10 @@
{ config, lib, ... }:
{
imports = [
./hosts.nix
./pia-openvpn.nix
./vpn.nix
./zerotier.nix
];
}

63
common/network/hosts.nix Normal file
View File

@@ -0,0 +1,63 @@
{ config, lib, ... }:
let
system = (import ../ssh.nix).system;
in {
networking.hosts = {
# some DNS providers filter local ip results from DNS request
"172.30.145.180" = [ "s0.zt.neet.dev" ];
"172.30.109.9" = [ "ponyo.zt.neet.dev" ];
"172.30.189.212" = [ "ray.zt.neet.dev" ];
};
programs.ssh.knownHosts = {
liza = {
hostNames = [ "liza" "liza.neet.dev" ];
publicKey = system.liza;
};
ponyo = {
hostNames = [ "ponyo" "ponyo.neet.dev" "ponyo.zt.neet.dev" "git.neet.dev" ];
publicKey = system.ponyo;
};
ponyo-unlock = {
hostNames = [ "unlock.ponyo.neet.dev" "cfamr6artx75qvt7ho3rrbsc7mkucmv5aawebwflsfuorusayacffryd.onion" ];
publicKey = system.ponyo-unlock;
};
ray = {
hostNames = [ "ray" "ray.zt.neet.dev" ];
publicKey = system.ray;
};
s0 = {
hostNames = [ "s0" "s0.zt.neet.dev" ];
publicKey = system.s0;
};
n1 = {
hostNames = [ "n1" ];
publicKey = system.n1;
};
n2 = {
hostNames = [ "n2" ];
publicKey = system.n2;
};
n3 = {
hostNames = [ "n3" ];
publicKey = system.n3;
};
n4 = {
hostNames = [ "n4" ];
publicKey = system.n4;
};
n5 = {
hostNames = [ "n5" ];
publicKey = system.n5;
};
n6 = {
hostNames = [ "n6" ];
publicKey = system.n6;
};
n7 = {
hostNames = [ "n7" ];
publicKey = system.n7;
};
};
}

View File

@@ -0,0 +1,113 @@
{ config, pkgs, lib, ... }:
let
cfg = config.pia;
vpnfailsafe = pkgs.stdenv.mkDerivation {
pname = "vpnfailsafe";
version = "0.0.1";
src = ./.;
installPhase = ''
mkdir -p $out
cp vpnfailsafe.sh $out/vpnfailsafe.sh
sed -i 's|getent|${pkgs.getent}/bin/getent|' $out/vpnfailsafe.sh
'';
};
in
{
options.pia = {
enable = lib.mkEnableOption "Enable private internet access";
server = lib.mkOption {
type = lib.types.str;
default = "us-washingtondc.privacy.network";
example = "swiss.privacy.network";
};
};
config = lib.mkIf cfg.enable {
services.openvpn = {
servers = {
pia = {
config = ''
client
dev tun
proto udp
remote ${cfg.server} 1198
resolv-retry infinite
nobind
persist-key
persist-tun
cipher aes-128-cbc
auth sha1
tls-client
remote-cert-tls server
auth-user-pass
compress
verb 1
reneg-sec 0
<crl-verify>
-----BEGIN X509 CRL-----
MIICWDCCAUAwDQYJKoZIhvcNAQENBQAwgegxCzAJBgNVBAYTAlVTMQswCQYDVQQI
EwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRl
cm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAw
HgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0
ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRl
aW50ZXJuZXRhY2Nlc3MuY29tFw0xNjA3MDgxOTAwNDZaFw0zNjA3MDMxOTAwNDZa
MCYwEQIBARcMMTYwNzA4MTkwMDQ2MBECAQYXDDE2MDcwODE5MDA0NjANBgkqhkiG
9w0BAQ0FAAOCAQEAQZo9X97ci8EcPYu/uK2HB152OZbeZCINmYyluLDOdcSvg6B5
jI+ffKN3laDvczsG6CxmY3jNyc79XVpEYUnq4rT3FfveW1+Ralf+Vf38HdpwB8EW
B4hZlQ205+21CALLvZvR8HcPxC9KEnev1mU46wkTiov0EKc+EdRxkj5yMgv0V2Re
ze7AP+NQ9ykvDScH4eYCsmufNpIjBLhpLE2cuZZXBLcPhuRzVoU3l7A9lvzG9mjA
5YijHJGHNjlWFqyrn1CfYS6koa4TGEPngBoAziWRbDGdhEgJABHrpoaFYaL61zqy
MR6jC0K2ps9qyZAN74LEBedEfK7tBOzWMwr58A==
-----END X509 CRL-----
</crl-verify>
<ca>
-----BEGIN CERTIFICATE-----
MIIFqzCCBJOgAwIBAgIJAKZ7D5Yv87qDMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYD
VQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNV
BAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIElu
dGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3Mx
IDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkB
FiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzM1
MThaFw0zNDA0MTIxNzM1MThaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex
EzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQg
QWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UE
AxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50
ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVy
bmV0YWNjZXNzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPXD
L1L9tX6DGf36liA7UBTy5I869z0UVo3lImfOs/GSiFKPtInlesP65577nd7UNzzX
lH/P/CnFPdBWlLp5ze3HRBCc/Avgr5CdMRkEsySL5GHBZsx6w2cayQ2EcRhVTwWp
cdldeNO+pPr9rIgPrtXqT4SWViTQRBeGM8CDxAyTopTsobjSiYZCF9Ta1gunl0G/
8Vfp+SXfYCC+ZzWvP+L1pFhPRqzQQ8k+wMZIovObK1s+nlwPaLyayzw9a8sUnvWB
/5rGPdIYnQWPgoNlLN9HpSmsAcw2z8DXI9pIxbr74cb3/HSfuYGOLkRqrOk6h4RC
OfuWoTrZup1uEOn+fw8CAwEAAaOCAVQwggFQMB0GA1UdDgQWBBQv63nQ/pJAt5tL
y8VJcbHe22ZOsjCCAR8GA1UdIwSCARYwggESgBQv63nQ/pJAt5tLy8VJcbHe22ZO
sqGB7qSB6zCB6DELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRMwEQYDVQQHEwpM
b3NBbmdlbGVzMSAwHgYDVQQKExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4G
A1UECxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAMTF1ByaXZhdGUg
SW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQpExdQcml2YXRlIEludGVybmV0IEFjY2Vz
czEvMC0GCSqGSIb3DQEJARYgc2VjdXJlQHByaXZhdGVpbnRlcm5ldGFjY2Vzcy5j
b22CCQCmew+WL/O6gzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4IBAQAn
a5PgrtxfwTumD4+3/SYvwoD66cB8IcK//h1mCzAduU8KgUXocLx7QgJWo9lnZ8xU
ryXvWab2usg4fqk7FPi00bED4f4qVQFVfGfPZIH9QQ7/48bPM9RyfzImZWUCenK3
7pdw4Bvgoys2rHLHbGen7f28knT2j/cbMxd78tQc20TIObGjo8+ISTRclSTRBtyC
GohseKYpTS9himFERpUgNtefvYHbn70mIOzfOJFTVqfrptf9jXa9N8Mpy3ayfodz
1wiqdteqFXkTYoSDctgKMiZ6GdocK9nMroQipIQtpnwd4yBDWIyC6Bvlkrq5TQUt
YDQ8z9v+DMO6iwyIDRiU
-----END CERTIFICATE-----
</ca>
disable-occ
auth-user-pass /run/agenix/pia-login.conf
'';
autoStart = true;
up = "${vpnfailsafe}/vpnfailsafe.sh";
down = "${vpnfailsafe}/vpnfailsafe.sh";
};
};
};
age.secrets."pia-login.conf".file = ../../secrets/pia-login.conf;
};
}

97
common/network/vpn.nix Normal file
View File

@@ -0,0 +1,97 @@
{ config, pkgs, lib, allModules, ... }:
with lib;
let
cfg = config.vpn-container;
in
{
options.vpn-container = {
enable = mkEnableOption "Enable VPN container";
containerName = mkOption {
type = types.str;
default = "vpn";
description = ''
Name of the VPN container.
'';
};
mounts = mkOption {
type = types.listOf types.str;
default = [ "/var/lib" ];
example = "/home/example";
description = ''
List of mounts on the host to bind to the vpn container.
'';
};
config = mkOption {
type = types.anything;
default = {};
example = ''
{
services.nginx.enable = true;
}
'';
description = ''
NixOS config for the vpn container.
'';
};
};
config = mkIf cfg.enable {
containers.${cfg.containerName} = {
ephemeral = true;
autoStart = true;
bindMounts = mkMerge ([{
"/run/agenix" = {
hostPath = "/run/agenix";
isReadOnly = true;
};
}] ++ (lists.forEach cfg.mounts (mount:
{
"${mount}" = {
hostPath = mount;
isReadOnly = false;
};
}
)));
enableTun = true;
privateNetwork = true;
hostAddress = "172.16.100.1";
localAddress = "172.16.100.2";
config = {
imports = allModules ++ [cfg.config];
nixpkgs.pkgs = pkgs;
networking.firewall.enable = mkForce false;
pia.enable = true;
pia.server = "swiss.privacy.network"; # swiss vpn
# run it's own DNS resolver
networking.useHostResolvConf = false;
services.resolved.enable = true;
};
};
# load secrets the container needs
age.secrets = config.containers.${cfg.containerName}.config.age.secrets;
# forwarding for vpn container
networking.nat.enable = true;
networking.nat.internalInterfaces = [
"ve-${cfg.containerName}"
];
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
# assumes only one potential interface
networking.usePredictableInterfaceNames = false;
networking.nat.externalInterface = "eth0";
};
}

187
common/network/vpnfailsafe.sh Executable file
View File

@@ -0,0 +1,187 @@
#!/usr/bin/env bash
set -eEo pipefail
# $@ := ""
set_route_vars() {
local network_var
local -a network_vars; read -ra network_vars <<<"${!route_network_*}"
for network_var in "${network_vars[@]}"; do
local -i i="${network_var#route_network_}"
local -a vars=("route_network_$i" "route_netmask_$i" "route_gateway_$i" "route_metric_$i")
route_networks[i]="${!vars[0]}"
route_netmasks[i]="${!vars[1]:-255.255.255.255}"
route_gateways[i]="${!vars[2]:-$route_vpn_gateway}"
route_metrics[i]="${!vars[3]:-0}"
done
}
# Configuration.
readonly prog="$(basename "$0")"
readonly private_nets="127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
declare -a remotes cnf_remote_domains cnf_remote_ips route_networks route_netmasks route_gateways route_metrics
read -ra remotes <<<"$(env|grep -oP '^remote_[0-9]+=.*'|sort -n|cut -d= -f2|tr '\n' '\t')"
read -ra cnf_remote_domains <<<"$(printf '%s\n' "${remotes[@]%%*[0-9]}"|sort -u|tr '\n' '\t')"
read -ra cnf_remote_ips <<<"$(printf '%s\n' "${remotes[@]##*[!0-9.]*}"|sort -u|tr '\n' '\t')"
set_route_vars
read -ra numbered_vars <<<"${!foreign_option_*} ${!proto_*} ${!remote_*} ${!remote_port_*} \
${!route_network_*} ${!route_netmask_*} ${!route_gateway_*} ${!route_metric_*}"
readonly numbered_vars "${numbered_vars[@]}" dev ifconfig_local ifconfig_netmask ifconfig_remote \
route_net_gateway route_vpn_gateway script_type trusted_ip trusted_port untrusted_ip untrusted_port \
remotes cnf_remote_domains cnf_remote_ips route_networks route_netmasks route_gateways route_metrics
readonly cur_remote_ip="${trusted_ip:-$untrusted_ip}"
readonly cur_port="${trusted_port:-$untrusted_port}"
# $@ := ""
update_hosts() {
if remote_entries="$(getent -s dns hosts "${cnf_remote_domains[@]}"|grep -v :)"; then
local -r beg="# VPNFAILSAFE BEGIN" end="# VPNFAILSAFE END"
{
sed -e "/^$beg/,/^$end/d" /etc/hosts
echo -e "$beg\\n$remote_entries\\n$end"
} >/etc/hosts.vpnfailsafe
chmod --reference=/etc/hosts /etc/hosts.vpnfailsafe
mv /etc/hosts.vpnfailsafe /etc/hosts
fi
}
# $@ := "up" | "down"
update_routes() {
local -a resolved_ips
read -ra resolved_ips <<<"$(getent -s files hosts "${cnf_remote_domains[@]:-ENOENT}"|cut -d' ' -f1|tr '\n' '\t' || true)"
local -ar remote_ips=("$cur_remote_ip" "${resolved_ips[@]}" "${cnf_remote_ips[@]}")
if [[ "$*" == up ]]; then
for remote_ip in "${remote_ips[@]}"; do
if [[ -n "$remote_ip" && -z "$(ip route show "$remote_ip")" ]]; then
ip route add "$remote_ip" via "$route_net_gateway"
fi
done
for net in 0.0.0.0/1 128.0.0.0/1; do
if [[ -z "$(ip route show "$net")" ]]; then
ip route add "$net" via "$route_vpn_gateway"
fi
done
for i in $(seq 1 "${#route_networks[@]}"); do
if [[ -z "$(ip route show "${route_networks[i]}/${route_netmasks[i]}")" ]]; then
ip route add "${route_networks[i]}/${route_netmasks[i]}" \
via "${route_gateways[i]}" metric "${route_metrics[i]}" dev "$dev"
fi
done
elif [[ "$*" == down ]]; then
for route in "${remote_ips[@]}" 0.0.0.0/1 128.0.0.0/1; do
if [[ -n "$route" && -n "$(ip route show "$route")" ]]; then
ip route del "$route"
fi
done
for i in $(seq 1 "${#route_networks[@]}"); do
if [[ -n "$(ip route show "${route_networks[i]}/${route_netmasks[i]}")" ]]; then
ip route del "${route_networks[i]}/${route_netmasks[i]}"
fi
done
fi
}
# $@ := ""
update_firewall() {
# $@ := "INPUT" | "OUTPUT" | "FORWARD"
insert_chain() {
if iptables -C "$*" -j "VPNFAILSAFE_$*" 2>/dev/null; then
iptables -D "$*" -j "VPNFAILSAFE_$*"
for opt in F X; do
iptables -"$opt" "VPNFAILSAFE_$*"
done
fi
iptables -N "VPNFAILSAFE_$*"
iptables -I "$*" -j "VPNFAILSAFE_$*"
}
# $@ := "INPUT" | "OUTPUT"
accept_remotes() {
case "$@" in
INPUT) local -r icmp_type=reply io=i sd=s states="";;
OUTPUT) local -r icmp_type=request io=o sd=d states=NEW,;;
esac
local -r public_nic="$(ip route show "$cur_remote_ip"|cut -d' ' -f5)"
local -ar suf=(-m conntrack --ctstate "$states"RELATED,ESTABLISHED -"$io" "${public_nic:?}" -j ACCEPT)
icmp_rule() {
iptables "$1" "$2" -p icmp --icmp-type "echo-$icmp_type" -"$sd" "$3" "${suf[@]/%ACCEPT/RETURN}"
}
for ((i=1; i <= ${#remotes[*]}; ++i)); do
local port="remote_port_$i"
local proto="proto_$i"
iptables -A "VPNFAILSAFE_$*" -p "${!proto%-client}" -"$sd" "${remotes[i-1]}" --"$sd"port "${!port}" "${suf[@]}"
if ! icmp_rule -C "VPNFAILSAFE_$*" "${remotes[i-1]}" 2>/dev/null; then
icmp_rule -A "VPNFAILSAFE_$*" "${remotes[i-1]}"
fi
done
if ! iptables -S|grep -q "^-A VPNFAILSAFE_$* .*-$sd $cur_remote_ip/32 .*-j ACCEPT$"; then
for p in tcp udp; do
iptables -A "VPNFAILSAFE_$*" -p "$p" -"$sd" "$cur_remote_ip" --"$sd"port "${cur_port}" "${suf[@]}"
done
icmp_rule -A "VPNFAILSAFE_$*" "$cur_remote_ip"
fi
}
# $@ := "OUTPUT" | "FORWARD"
reject_dns() {
for proto in udp tcp; do
iptables -A "VPNFAILSAFE_$*" -p "$proto" --dport 53 ! -o "$dev" -j REJECT
done
}
# $@ := "INPUT" | "OUTPUT" | "FORWARD"
pass_private_nets() {
case "$@" in
INPUT) local -r io=i sd=s;;&
OUTPUT|FORWARD) local -r io=o sd=d;;&
INPUT) local -r vpn="${ifconfig_remote:-$ifconfig_local}/${ifconfig_netmask:-32}"
iptables -A "VPNFAILSAFE_$*" -"$sd" "$vpn" -"$io" "$dev" -j RETURN
for i in $(seq 1 "${#route_networks[@]}"); do
iptables -A "VPNFAILSAFE_$*" -"$sd" "${route_networks[i]}/${route_netmasks[i]}" -"$io" "$dev" -j RETURN
done;;&
*) iptables -A "VPNFAILSAFE_$*" -"$sd" "$private_nets" ! -"$io" "$dev" -j RETURN;;&
INPUT) iptables -A "VPNFAILSAFE_$*" -s "$private_nets" -i "$dev" -j DROP;;&
*) for iface in "$dev" lo+; do
iptables -A "VPNFAILSAFE_$*" -"$io" "$iface" -j RETURN
done;;
esac
}
# $@ := "INPUT" | "OUTPUT" | "FORWARD"
drop_other() {
iptables -A "VPNFAILSAFE_$*" -j DROP
}
for chain in INPUT OUTPUT FORWARD; do
insert_chain "$chain"
[[ $chain == FORWARD ]] || accept_remotes "$chain"
[[ $chain == INPUT ]] || reject_dns "$chain"
pass_private_nets "$chain"
drop_other "$chain"
done
}
# $@ := ""
cleanup() {
update_resolv down
update_routes down
}
trap cleanup INT TERM
# $@ := line_number exit_code
err_msg() {
echo "$0:$1: \`$(sed -n "$1,+0{s/^\\s*//;p}" "$0")' returned $2" >&2
cleanup
}
trap 'err_msg "$LINENO" "$?"' ERR
# $@ := ""
main() {
case "${script_type:-down}" in
up) for f in hosts routes firewall; do "update_$f" up; done;;
down) update_routes down
update_resolv down;;
esac
}
main

View File

@@ -0,0 +1,14 @@
{ lib, config, ... }:
let
cfg = config.services.zerotierone;
in {
config = lib.mkIf cfg.enable {
services.zerotierone.joinNetworks = [
"565799d8f6d654c0"
];
networking.firewall.allowedUDPPorts = [
9993
];
};
}