firmware: Working WireGuard management

This commit is contained in:
Jack O'Sullivan 2024-09-01 19:21:09 +01:00
parent 550445e0f9
commit 631e228bd5
7 changed files with 247 additions and 24 deletions

2
firmware/.gitignore vendored
View File

@ -1,2 +1,2 @@
/result /result
/*.img /*.img*

31
firmware/app.nix Normal file
View File

@ -0,0 +1,31 @@
{ lib, pkgs, config, ... }:
let
configurer = pkgs.substituteAll {
src = ./configurer.py;
isExecutable = true;
python = pkgs.python3.withPackages (ps: with ps; [ pyyaml ]);
wireguardTools = pkgs.wireguard-tools;
systemd = config.systemd.package;
};
in
{
config = {
systemd = {
services = {
qclk-configurer = {
description = "qclk dynamic configurer";
after = [ "network.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${configurer}";
};
wantedBy = [ "multi-user.target" ];
};
};
};
};
}

View File

@ -11,7 +11,7 @@ in
}; };
name = "qclk"; name = "qclk";
}; };
documentation.nixos.enable = false; documentation.enable = false;
time.timeZone = "Europe/Dublin"; time.timeZone = "Europe/Dublin";
i18n.defaultLocale = "en_IE.UTF-8"; i18n.defaultLocale = "en_IE.UTF-8";

102
firmware/configurer.py Executable file
View File

@ -0,0 +1,102 @@
#! @python@/bin/python -B
import base64
import json
import os
import shutil
import socket
import subprocess
import sys
import yaml
CONF_FILE = '/etc/qclk/config.yaml'
WG_KEY_FILE = '/etc/qclk/wg.key'
IWD_PATH = '/var/lib/iwd'
IWD_CONF = '''[Settings]
Hidden={hidden}
[Security]
Passphrase={psk}
'''
MANAGEMENT_NET = '''[Match]
Name=management
[Network]
Address={ip}/32
'''
def log(m):
print(m, file=sys.stderr)
def iwd_ssid_basename(ssid: str) -> str:
if ssid.isalnum() and not any(c in ssid for c in '-_ '):
return ssid
return f"={ssid.encode('utf-8').hex()}"
class Configurer:
tmpdir = '/tmp/qclk'
def __init__(self, conf_file: str, wg_key_file: str):
with open(conf_file) as f:
self.config = yaml.safe_load(f)
with open(wg_key_file, 'rb') as f:
output = subprocess.check_output(['@wireguardTools@/bin/wg', 'pubkey'], input=f.read())
pubkey = base64.b64decode(output)
self.id = pubkey[:4].hex()
os.makedirs(self.tmpdir, exist_ok=True)
def _setup_hostname(self):
hostname = f'qclk-{self.id}'
log(f"Setting hostname to '{hostname}'")
socket.sethostname(hostname)
def _setup_wifi(self):
os.makedirs(IWD_CONF, exist_ok=True)
if 'wifi' in self.config:
conf = self.config['wifi']
tmp = os.path.join(self.tmpdir, 'wifi.psk')
with open(tmp, 'w') as f:
print(
IWD_CONF.format(
hidden=str(conf['hidden']).lower(),
psk=conf['password']),
file=f)
fname = f"{iwd_ssid_basename(conf['ssid'])}.psk"
log(f"Writing IWD config file '{fname}' for network '{conf['ssid']}'")
shutil.move(tmp, os.path.join(IWD_PATH, fname))
else:
fname = None
for f in os.listdir(IWD_PATH):
if (fname is not None and f != fname) and f.endswith('.psk'):
log(f"Cleaning up old IWD config '{f}'")
os.remove(os.path.join(IWD_PATH, f))
def _setup_mgmt(self):
conf = self.config['management']
tmp = os.path.join(self.tmpdir, 'management.network')
with open(tmp, 'w') as f:
print(MANAGEMENT_NET.format(ip=conf['ip']), file=f)
log(f"Configuring management IP {conf['ip']}")
os.makedirs('/run/systemd/network', exist_ok=True)
shutil.move(tmp, '/run/systemd/network/20-management.network')
subprocess.check_call(['@systemd@/bin/networkctl', 'reload'])
def reconcile(self):
self._setup_hostname()
self._setup_wifi()
self._setup_mgmt()
def main():
c = Configurer(CONF_FILE, WG_KEY_FILE)
c.reconcile()
if __name__ == '__main__':
main()

View File

@ -26,6 +26,7 @@ let
./base.nix ./base.nix
./disk.nix ./disk.nix
./network.nix ./network.nix
./app.nix
target target
]; ];
@ -36,49 +37,107 @@ in
qclk-rpi3 = mkSystem target/rpi3.nix; qclk-rpi3 = mkSystem target/rpi3.nix;
}; };
perSystem = { libMy, pkgs, ... }: { perSystem = { lib, libMy, pkgs, ... }:
let
inherit (lib) concatMapStringsSep;
in
{
devenv.shells.firmware = libMy.withRootdir { devenv.shells.firmware = libMy.withRootdir {
packages = with pkgs; [ packages = with pkgs; [
nixos-rebuild nixos-rebuild
nixVersions.latest nixVersions.latest
wireguard-tools
]; ];
scripts = { scripts =
let
shellUtils = ''
die() {
echo "$1" >&2
exit 1
}
'';
exportPath = ps:
let pkgsPath = concatMapStringsSep ":" (p: "${p}/bin") ps; in ''export PATH="$PATH:${pkgsPath}"'';
buildImgRootHelper = pkgs.writeShellScript "fixup-perms" ''
pushd "$1"
# systemd-network GID
chgrp 152 etc/qclk/wg.key
chmod 640 etc/qclk/wg.key
popd
mkfs.ext4 -L qclkos-persist -d "$1" "$2"
'';
in
{
build.exec = '' build.exec = ''
nix build "..#nixosConfigurations.qclk-$1.config.system.build.toplevel" nix build "..#nixosConfigurations.qclk-$1.config.system.build.toplevel"
''; '';
build-image.exec = '' build-image.exec = ''
set -e set -e
export PATH="$PATH:${pkgs.util-linux}/bin:${pkgs.fakeroot}/bin:${pkgs.e2fsprogs}/bin" ${exportPath (with pkgs; [ util-linux fakeroot e2fsprogs zstd python3 ])}
die() { ${shellUtils}
echo "$1" >&2 usage() {
exit 1 die "usage: $0 <target> <IP>"
} }
[ -z "$1" ] && die "Need to set target" populate_persist() {
mkdir -p etc/qclk
old_umask="$(umask)"
umask 077
wg genkey > etc/qclk/wg.key
wg_pubkey="$(wg pubkey < etc/qclk/wg.key)"
id="$(python3 -c "import base64; print(base64.b64decode('$wg_pubkey')[:4].hex())")"
pin="$(python3 -c "import random; print('''.join(str(random.randint(0, 9)) for _ in range(6)))")"
cat << EOF > etc/qclk/config.yaml
management:
ip: $ip
pin: '$pin'
EOF
umask "$old_umask"
}
[ $# -eq 2 ] || usage
target=$1 target=$1
out=qclkos-$target.img ip=$2
nix build "..#nixosConfigurations.qclk-$target.config.my.disk.image" nix build "..#nixosConfigurations.qclk-$target.config.my.disk.image"
persistRoot=$(mktemp --tmpdir -d qclkos-persist-XXXXX) persistRoot=$(mktemp --tmpdir -d qclkos-persist-XXXXX)
# TODO: bless with unique stuff (e.g. keys) pushd "$persistRoot"
touch "$persistRoot"/test.txt populate_persist
popd
cp --sparse=always result/$out $out out=qclkos-$target-$id.img
cp --sparse=always result/qclkos-$target.img $out
chmod u+w $out chmod u+w $out
eval $(partx $out -o START,SECTORS --nr 2 --pairs) eval $(partx $out -o START,SECTORS --nr 2 --pairs)
persistImg=$(mktemp --tmpdir qclkos-persist-XXXXX.img) persistImg=$(mktemp --tmpdir qclkos-persist-XXXXX.img)
truncate -s $((SECTORS * 512)) $persistImg truncate -s $((SECTORS * 512)) $persistImg
fakeroot mkfs.ext4 -L qclkos-persist -d $persistRoot $persistImg fakeroot ${buildImgRootHelper} $persistRoot $persistImg
dd conv=notrunc if=$persistImg of=$out seek=$START count=$SECTORS dd conv=notrunc if=$persistImg of=$out seek=$START count=$SECTORS
rm -r "$persistRoot" "$persistImg" rm -r "$persistRoot" "$persistImg"
[ -z "$NO_COMPRESS" ] && zstd -7 -T0 --rm -f $out
echo "====== DONE! ======"
echo "WireGuard pubkey: $wg_pubkey"
echo "Control PIN: $pin"
''; '';
push-config.exec = '' push-config.exec = ''
${shellUtils}
usage() {
die "usage: $0 <host> <target> <verb>"
}
[ $# -eq 3 ] || usage
host=$1; shift host=$1; shift
target=$1; shift target=$1; shift
verb=$1; shift verb=$1; shift
@ -86,6 +145,9 @@ in
export NIX_SSHOPTS="-i .keys/management.key" export NIX_SSHOPTS="-i .keys/management.key"
nixos-rebuild $verb --flake ..#qclk-$target --target-host root@"$host" --use-substitutes "$@" nixos-rebuild $verb --flake ..#qclk-$target --target-host root@"$host" --use-substitutes "$@"
''; '';
clean.exec = ''
rm -f qclkos-*.img*
'';
}; };
}; };
}; };

View File

@ -12,15 +12,16 @@ let
compressImage = false; compressImage = false;
}).overrideAttrs (o: { }).overrideAttrs (o: {
buildCommand = '' buildCommand = ''
# HACK: `populateImageCommands` is executed in a subshell _before_ the paths are copied in... # We need to move the store up a level since we're not using this as a rootfs but as /nix
# HACK: `populateImageCommands` is executed in a subshell _before_ the store paths are copied in...
shopt -s expand_aliases shopt -s expand_aliases
nixUpThenMkfs() { nixUpThenFaketime() {
mv rootImage/{nix/store,} mv rootImage/{nix/store,}
rmdir rootImage/nix rmdir rootImage/nix
faketime "$@" faketime "$@"
} }
alias faketime=nixUpThenMkfs alias faketime=nixUpThenFaketime
${o.buildCommand} ${o.buildCommand}
''; '';
@ -123,6 +124,8 @@ in
directories = [ directories = [
"/var/lib/nixos" "/var/lib/nixos"
"/var/lib/systemd" "/var/lib/systemd"
"/etc/qclk"
]; ];
files = [ files = [
"/etc/machine-id" "/etc/machine-id"

View File

@ -1,27 +1,52 @@
{ lib, config, ... }: { { lib, config, pkgs, ... }: {
environment = {
systemPackages = with pkgs; [
wireguard-tools
];
};
networking = { networking = {
hostName = config.system.name; hostName = config.system.name;
useDHCP = false; useDHCP = false;
useNetworkd = true; useNetworkd = true;
wireless.iwd = { wireless.iwd = {
enable = true; enable = true;
settings.DriverQuirks.DefaultInterface = "*"; settings = {
# systemd-networkd gets confused if we hop between networks and doesn't redo DHCP
General.EnableNetworkConfiguration = true;
DriverQuirks.DefaultInterface = "*";
};
}; };
}; };
systemd = { systemd = {
network = { network = {
wait-online.enable = false; wait-online.enable = false;
netdevs = {
"10-management" = {
netdevConfig = {
Name = "management";
Kind = "wireguard";
};
wireguardConfig = {
PrivateKeyFile = "/etc/qclk/wg.key";
RouteTable = "main";
};
wireguardPeers = [
{
Endpoint = "94.142.240.44:51821";
PublicKey = "itMQ2DlPEMdJFlIZRQkwa+Mv7cLc9d4zgfzlljEtLn4=";
AllowedIPs = [ "10.100.4.1/32" ];
PersistentKeepalive = 15;
}
];
};
};
networks = { networks = {
"10-ethernet" = { "10-ethernet" = {
matchConfig.Name = "ethernet"; matchConfig.Name = "ethernet";
DHCP = "yes"; DHCP = "yes";
}; };
"10-wifi" = {
matchConfig.Name = "wifi";
DHCP = "yes";
networkConfig.IgnoreCarrierLoss = "3s";
};
}; };
}; };
}; };