firmware: Working WireGuard management
This commit is contained in:
parent
550445e0f9
commit
631e228bd5
2
firmware/.gitignore
vendored
2
firmware/.gitignore
vendored
@ -1,2 +1,2 @@
|
||||
/result
|
||||
/*.img
|
||||
/*.img*
|
||||
|
31
firmware/app.nix
Normal file
31
firmware/app.nix
Normal 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" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
@ -11,7 +11,7 @@ in
|
||||
};
|
||||
name = "qclk";
|
||||
};
|
||||
documentation.nixos.enable = false;
|
||||
documentation.enable = false;
|
||||
|
||||
time.timeZone = "Europe/Dublin";
|
||||
i18n.defaultLocale = "en_IE.UTF-8";
|
||||
|
102
firmware/configurer.py
Executable file
102
firmware/configurer.py
Executable 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()
|
@ -26,6 +26,7 @@ let
|
||||
./base.nix
|
||||
./disk.nix
|
||||
./network.nix
|
||||
./app.nix
|
||||
|
||||
target
|
||||
];
|
||||
@ -36,49 +37,107 @@ in
|
||||
qclk-rpi3 = mkSystem target/rpi3.nix;
|
||||
};
|
||||
|
||||
perSystem = { libMy, pkgs, ... }: {
|
||||
perSystem = { lib, libMy, pkgs, ... }:
|
||||
let
|
||||
inherit (lib) concatMapStringsSep;
|
||||
in
|
||||
{
|
||||
devenv.shells.firmware = libMy.withRootdir {
|
||||
packages = with pkgs; [
|
||||
nixos-rebuild
|
||||
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 = ''
|
||||
nix build "..#nixosConfigurations.qclk-$1.config.system.build.toplevel"
|
||||
'';
|
||||
build-image.exec = ''
|
||||
set -e
|
||||
export PATH="$PATH:${pkgs.util-linux}/bin:${pkgs.fakeroot}/bin:${pkgs.e2fsprogs}/bin"
|
||||
die() {
|
||||
echo "$1" >&2
|
||||
exit 1
|
||||
${exportPath (with pkgs; [ util-linux fakeroot e2fsprogs zstd python3 ])}
|
||||
${shellUtils}
|
||||
usage() {
|
||||
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
|
||||
out=qclkos-$target.img
|
||||
ip=$2
|
||||
|
||||
nix build "..#nixosConfigurations.qclk-$target.config.my.disk.image"
|
||||
|
||||
persistRoot=$(mktemp --tmpdir -d qclkos-persist-XXXXX)
|
||||
# TODO: bless with unique stuff (e.g. keys)
|
||||
touch "$persistRoot"/test.txt
|
||||
pushd "$persistRoot"
|
||||
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
|
||||
|
||||
eval $(partx $out -o START,SECTORS --nr 2 --pairs)
|
||||
persistImg=$(mktemp --tmpdir qclkos-persist-XXXXX.img)
|
||||
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
|
||||
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 = ''
|
||||
${shellUtils}
|
||||
usage() {
|
||||
die "usage: $0 <host> <target> <verb>"
|
||||
}
|
||||
[ $# -eq 3 ] || usage
|
||||
|
||||
host=$1; shift
|
||||
target=$1; shift
|
||||
verb=$1; shift
|
||||
@ -86,6 +145,9 @@ in
|
||||
export NIX_SSHOPTS="-i .keys/management.key"
|
||||
nixos-rebuild $verb --flake ..#qclk-$target --target-host root@"$host" --use-substitutes "$@"
|
||||
'';
|
||||
clean.exec = ''
|
||||
rm -f qclkos-*.img*
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -12,15 +12,16 @@ let
|
||||
compressImage = false;
|
||||
}).overrideAttrs (o: {
|
||||
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
|
||||
nixUpThenMkfs() {
|
||||
nixUpThenFaketime() {
|
||||
mv rootImage/{nix/store,}
|
||||
rmdir rootImage/nix
|
||||
|
||||
faketime "$@"
|
||||
}
|
||||
alias faketime=nixUpThenMkfs
|
||||
alias faketime=nixUpThenFaketime
|
||||
|
||||
${o.buildCommand}
|
||||
'';
|
||||
@ -123,6 +124,8 @@ in
|
||||
directories = [
|
||||
"/var/lib/nixos"
|
||||
"/var/lib/systemd"
|
||||
|
||||
"/etc/qclk"
|
||||
];
|
||||
files = [
|
||||
"/etc/machine-id"
|
||||
|
@ -1,27 +1,52 @@
|
||||
{ lib, config, ... }: {
|
||||
{ lib, config, pkgs, ... }: {
|
||||
environment = {
|
||||
systemPackages = with pkgs; [
|
||||
wireguard-tools
|
||||
];
|
||||
};
|
||||
|
||||
networking = {
|
||||
hostName = config.system.name;
|
||||
useDHCP = false;
|
||||
useNetworkd = true;
|
||||
wireless.iwd = {
|
||||
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 = {
|
||||
network = {
|
||||
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 = {
|
||||
"10-ethernet" = {
|
||||
matchConfig.Name = "ethernet";
|
||||
DHCP = "yes";
|
||||
};
|
||||
"10-wifi" = {
|
||||
matchConfig.Name = "wifi";
|
||||
DHCP = "yes";
|
||||
networkConfig.IgnoreCarrierLoss = "3s";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user