Make Wi-Fi semi-declarative

This commit is contained in:
Geoffrey Frogeye 2024-06-08 15:54:33 +02:00
parent bc53468373
commit 96dea140be
Signed by: geoffrey
GPG key ID: C72403E7F82E6AD8
5 changed files with 125 additions and 126 deletions

View file

@ -155,6 +155,22 @@
"type": "indirect" "type": "indirect"
} }
}, },
"displaylinknixpkgs": {
"locked": {
"lastModified": 1717533296,
"narHash": "sha256-TOxOpOYy/tQB+eYQOTPQXNeUmkMghLVDBO0Gc2nj/vs=",
"owner": "GeoffreyFrogeye",
"repo": "nixpkgs",
"rev": "99006b6f4cd24796b1ff6b6981b8f44c9cebd301",
"type": "github"
},
"original": {
"owner": "GeoffreyFrogeye",
"ref": "displaylink-600",
"repo": "nixpkgs",
"type": "github"
}
},
"flake-compat": { "flake-compat": {
"locked": { "locked": {
"lastModified": 1696426674, "lastModified": 1696426674,
@ -649,6 +665,7 @@
"root": { "root": {
"inputs": { "inputs": {
"disko": "disko", "disko": "disko",
"displaylinknixpkgs": "displaylinknixpkgs",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"home-manager": "home-manager", "home-manager": "home-manager",
"nix-on-droid": "nix-on-droid", "nix-on-droid": "nix-on-droid",

View file

@ -95,7 +95,7 @@
repl = { repl = {
type = "app"; type = "app";
program = "${pkgs.writeShellScript "vivarium-repl" '' program = "${pkgs.writeShellScript "vivarium-repl" ''
${pkgs.nix}/bin/nix repl --expr 'let flake = builtins.getFlake "${self}"; in flake // flake.nixosConfigurations // rec { pkgs = import ${nixpkgs} {}; lib = pkgs.lib; }' ${pkgs.lix}/bin/nix repl --expr 'let flake = builtins.getFlake "${self}"; in flake // flake.nixosConfigurations // rec { pkgs = import ${nixpkgs} {}; lib = pkgs.lib; }'
''}"; ''}";
}; };
}; };

56
os/wireless/apply.py Normal file
View file

@ -0,0 +1,56 @@
"""
"""
import json
import os
import re
import subprocess
NETWORKS_FILE = "/etc/keys/wireless_networks.json"
def wpa_cli(command: list[str]) -> list[bytes]:
command.insert(0, "wpa_cli")
process = subprocess.run(command, stdout=subprocess.PIPE)
process.check_returncode()
lines = process.stdout.splitlines()
while lines[0].startswith(b"Selected interface"):
lines.pop(0)
return lines
network_numbers: dict[str, int] = dict()
networks_tsv = wpa_cli(["list_networks"])
networks_tsv.pop(0)
for network_line in networks_tsv:
split = network_line.split(b"\t")
number = int(split[0])
ssid_bytes = split[1]
ssid = re.sub(
rb"\\x([0-9a-f]{2})",
lambda d: int(d[1], base=16).to_bytes(),
ssid_bytes,
).decode()
network_numbers[ssid] = number
if os.path.isfile(NETWORKS_FILE):
with open(NETWORKS_FILE) as fd:
networks = json.load(fd)
for network in networks:
ssid = network["ssid"]
if ssid in network_numbers:
number = network_numbers[ssid]
else:
number = int(wpa_cli(["add_network"])[0])
number_str = str(number)
for key, value in network.items():
if isinstance(value, str):
value_str = f'"{value}"'
elif isinstance(value, list):
value_str = " ".join(value)
else:
value_str = str(value)
ret = wpa_cli(["set_network", number_str, key, value_str])
if ret[0] != b"OK":
raise RuntimeError(f"Couldn't set {key} for {ssid}, got {ret}")

View file

@ -1,34 +1,49 @@
{ pkgs, lib, config, ... }: { pkgs, lib, config, ... }:
let
importScript = pkgs.writers.writePython3 "install-wifi-import"
{
libraries = [ pkgs.python3Packages.pyaml ];
}
(builtins.readFile ./import.py);
applyScript = pkgs.writers.writePython3 "install-wifi-apply" { } (builtins.readFile ./apply.py);
in
{ {
environment.systemPackages = [ environment.systemPackages = [
(pkgs.writeShellApplication { (pkgs.writeShellApplication {
name = "install-wifi"; name = "install-wifi";
runtimeInputs = with pkgs; [ wpa_supplicant diffutils ];
text = '' text = ''
temp="$(mktemp --directory --suffix="-install-wifi")" temp="$(mktemp --directory --suffix="-install-wifi")"
cd "$temp" cd "$temp"
${
pkgs.writers.writePython3 "install-wifi-import" { # Save config for diffing later
libraries = [ pkgs.python3Packages.pyaml ]; wpa_cli save_config > /dev/null
} (builtins.readFile ./import.py) cat <(sudo cat /run/wpa_supplicant/wpa_supplicant.conf) > old.conf
}
sudo chown root:root wireless_networks.{env,json} # Export Wi-Fi config from pass
sudo chmod "u=r" wireless_networks.env ${importScript}
sudo chmod "u=r,g=r,o=r" wireless_networks.json
# Save on persistent storage for boot
sudo chown root:root wireless_networks.json
sudo chmod "u=r" wireless_networks.json
sudo mkdir -p /etc/keys sudo mkdir -p /etc/keys
sudo mv -f wireless_networks.{env,json} /etc/keys sudo mv -f wireless_networks.json /etc/keys
cd -
# Apply configuration
sudo ${applyScript}
# Diff the config
wpa_cli save_config > /dev/null
cat <(sudo cat /run/wpa_supplicant/wpa_supplicant.conf) > new.conf
diff --color=auto -U 5 old.conf new.conf
rm old.conf new.conf
cd /
rmdir "$temp" rmdir "$temp"
rb
''; '';
# This relies on multiple off-repo things: # This relies on pass password store with wifi/${name} entries,
# - pass password store with wifi/${name} entries, containing wpa_supplicant networks # containing wpa_supplicant networks loosely converted to YAML
# loosely converted to YAML (see import.py script) # (see import.py script)
# - In a (private) flake:
# inputs.wirelessNetworks.url = "path:/etc/keys/wireless_networks.json";
# inputs.wirelessNetworks.flake = false;
# - In NixOS config (using flake inputs):
# networking.wireless.environmentFile = "/etc/keys/wireless_networks.env";
# networking.wireless.networks = builtins.fromJSON (builtins.readFile wirelessNetworks);
}) })
]; ];
# wireless support via wpa_supplicant # wireless support via wpa_supplicant
@ -51,4 +66,10 @@
userControlled.enable = true; # Allow some control with wpa_cli userControlled.enable = true; # Allow some control with wpa_cli
}; };
services.chrony.serverOption = "offline"; services.chrony.serverOption = "offline";
systemd.services.wifi_apply = {
after = [ "wpa_supplicant.service" ];
requiredBy = [ "wpa_supplicant.service" ];
path = with pkgs; [ wpa_supplicant ];
script = "${applyScript}";
};
} }

View file

@ -9,7 +9,6 @@ into a format readable by Nix.
# sha256 = "sha256:1la36n2f31j9s03v847ig6ny9lr875q3g7smnq33dcsmf2i5gd92"; # sha256 = "sha256:1la36n2f31j9s03v847ig6ny9lr875q3g7smnq33dcsmf2i5gd92";
# } # }
import hashlib
import json import json
import os import os
import subprocess import subprocess
@ -20,46 +19,6 @@ import yaml
PASSWORD_STORE = os.environ["PASSWORD_STORE_DIR"] PASSWORD_STORE = os.environ["PASSWORD_STORE_DIR"]
SUBFOLDER = "wifi" SUBFOLDER = "wifi"
SEPARATE_PASSWORDS = True
class Password:
all: list["Password"] = list()
def __init__(self, path: str, content: str):
self.path = path
self.content = content
Password.all.append(self)
def var(self) -> str:
# return self.path.split("/")[-1].upper()
m = hashlib.sha256()
m.update(self.path.encode())
return "p" + m.hexdigest().upper()
def val(self) -> str:
return self.content
def exists(self) -> bool:
return not not self.content
def key(self) -> str:
if SEPARATE_PASSWORDS:
return f"@{self.var()}@"
else:
return self.val()
@classmethod
def vars(cls) -> dict[str, str]:
vars = dict()
for password in cls.all:
if not password.content:
continue
var = password.var()
assert var not in vars, f"Duplicate key: {var}"
vars[var] = password.val()
return vars
def list_networks() -> list[str]: def list_networks() -> list[str]:
@ -75,28 +34,11 @@ def list_networks() -> list[str]:
file = filename[:-4] file = filename[:-4]
path = os.path.join(SUBFOLDER, file) path = os.path.join(SUBFOLDER, file)
paths.append(path) paths.append(path)
paths.sort()
return paths return paths
def format_wpa_supplicant_conf(conf: dict, indent: str = "") -> str: networks: list[dict[str, str | list[str] | int]] = list()
lines = []
for k, v in conf.items():
if isinstance(v, str):
val = '"' + v.replace('"', '\\"') + '"'
elif isinstance(v, Password):
val = v.key()
elif isinstance(v, list):
assert all(
map(lambda i: isinstance(i, str), v)
), "Only list of strings supported"
val = " ".join(v)
else:
val = str(v)
lines.append(f"{indent}{k}={val}")
return "\n".join(lines)
networks = {}
for path in list_networks(): for path in list_networks():
proc = subprocess.run(["pass", path], stdout=subprocess.PIPE) proc = subprocess.run(["pass", path], stdout=subprocess.PIPE)
proc.check_returncode() proc.check_returncode()
@ -104,61 +46,24 @@ for path in list_networks():
raw = proc.stdout.decode() raw = proc.stdout.decode()
split = raw.split("\n") split = raw.split("\n")
password = Password(path, split[0]) password = split[0]
data = yaml.safe_load("\n".join(split[1:])) or dict() data = yaml.safe_load("\n".join(split[1:])) or dict()
# print(path, data) # DEBUG
# Helpers to prevent repetition # Helpers to prevent repetition
suffixes = data.pop("suffixes", [""]) suffixes = data.pop("suffixes", [""])
data.setdefault("key_mgmt", ["WPA-PSK"] if password.exists() else ["NONE"]) data.setdefault("key_mgmt", ["WPA-PSK"] if password else ["NONE"])
if password: if password:
if any(map(lambda m: "PSK" in m.split("-"), data["key_mgmt"])): if any(map(lambda m: "PSK" in m.split("-"), data["key_mgmt"])):
data["psk"] = password data["psk"] = password
if "NONE" in data["key_mgmt"]:
data["wep_key0"] = password
if any(map(lambda m: "EAP" in m.split("-"), data["key_mgmt"])): if any(map(lambda m: "EAP" in m.split("-"), data["key_mgmt"])):
data["password"] = password data["password"] = password
assert "ssid" in data, f"{path}: Missing SSID" assert "ssid" in data, f"{path}: Missing SSID"
data.setdefault("disabled", 0)
# # Output wpa_supplicant conf, for debug
# for suffix in suffixes:
# wpas = data.copy()
# wpas["ssid"] += suffix
# print(f"# {path}")
# print("network={")
# print(format_wpa_supplicant_conf(wpas, indent=" "))
# print("}")
# print()
# Convert to nix configuration
ssid = data.pop("ssid")
network = {}
key_mgmt = data.pop("key_mgmt", None)
psk = data.pop("psk", None)
priority = data.pop("priority", None)
# No support for hidden
# No support for extraConfig (all is assumed to be auth)
if key_mgmt:
network["authProtocols"] = key_mgmt
if psk:
network["psk"] = psk.key()
if data:
raise NotImplementedError(
f"{path}: Unhandled non-auth extra: {data}"
)
else:
if data:
network["auth"] = format_wpa_supplicant_conf(data)
if priority:
network["priority"] = int(priority)
for suffix in suffixes: for suffix in suffixes:
networks[ssid + suffix] = network network = data.copy()
network["ssid"] += suffix
networks.append(network)
with open("wireless_networks.json", "w") as fd: with open("wireless_networks.json", "w") as fd:
json.dump(networks, fd, indent=4) json.dump(networks, fd, indent=4)
with open("wireless_networks.env", "w") as fd:
if SEPARATE_PASSWORDS:
for k, v in Password.vars().items():
print(f'{k}="{v}"', file=fd)