Make Wi-Fi semi-declarative
This commit is contained in:
parent
bc53468373
commit
96dea140be
17
flake.lock
17
flake.lock
|
@ -155,6 +155,22 @@
|
|||
"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": {
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
|
@ -649,6 +665,7 @@
|
|||
"root": {
|
||||
"inputs": {
|
||||
"disko": "disko",
|
||||
"displaylinknixpkgs": "displaylinknixpkgs",
|
||||
"flake-utils": "flake-utils",
|
||||
"home-manager": "home-manager",
|
||||
"nix-on-droid": "nix-on-droid",
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
repl = {
|
||||
type = "app";
|
||||
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; }'
|
||||
''}";
|
||||
};
|
||||
};
|
||||
|
@ -128,4 +128,4 @@
|
|||
};
|
||||
nixOnDroidConfigurations.sprinkles = lib.nixOnDroidConfiguration { };
|
||||
} // (lib.flakeTools { inherit self; });
|
||||
}
|
||||
}
|
||||
|
|
56
os/wireless/apply.py
Normal file
56
os/wireless/apply.py
Normal 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}")
|
|
@ -1,34 +1,49 @@
|
|||
{ 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 = [
|
||||
(pkgs.writeShellApplication {
|
||||
name = "install-wifi";
|
||||
runtimeInputs = with pkgs; [ wpa_supplicant diffutils ];
|
||||
text = ''
|
||||
temp="$(mktemp --directory --suffix="-install-wifi")"
|
||||
cd "$temp"
|
||||
${
|
||||
pkgs.writers.writePython3 "install-wifi-import" {
|
||||
libraries = [ pkgs.python3Packages.pyaml ];
|
||||
} (builtins.readFile ./import.py)
|
||||
}
|
||||
sudo chown root:root wireless_networks.{env,json}
|
||||
sudo chmod "u=r" wireless_networks.env
|
||||
sudo chmod "u=r,g=r,o=r" wireless_networks.json
|
||||
|
||||
# Save config for diffing later
|
||||
wpa_cli save_config > /dev/null
|
||||
cat <(sudo cat /run/wpa_supplicant/wpa_supplicant.conf) > old.conf
|
||||
|
||||
# Export Wi-Fi config from pass
|
||||
${importScript}
|
||||
|
||||
# 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 mv -f wireless_networks.{env,json} /etc/keys
|
||||
cd -
|
||||
sudo mv -f wireless_networks.json /etc/keys
|
||||
|
||||
# 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"
|
||||
rb
|
||||
'';
|
||||
# This relies on multiple off-repo things:
|
||||
# - pass password store with wifi/${name} entries, containing wpa_supplicant networks
|
||||
# loosely converted to YAML (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);
|
||||
# This relies on pass password store with wifi/${name} entries,
|
||||
# containing wpa_supplicant networks loosely converted to YAML
|
||||
# (see import.py script)
|
||||
})
|
||||
];
|
||||
# wireless support via wpa_supplicant
|
||||
|
@ -51,4 +66,10 @@
|
|||
userControlled.enable = true; # Allow some control with wpa_cli
|
||||
};
|
||||
services.chrony.serverOption = "offline";
|
||||
systemd.services.wifi_apply = {
|
||||
after = [ "wpa_supplicant.service" ];
|
||||
requiredBy = [ "wpa_supplicant.service" ];
|
||||
path = with pkgs; [ wpa_supplicant ];
|
||||
script = "${applyScript}";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ into a format readable by Nix.
|
|||
# sha256 = "sha256:1la36n2f31j9s03v847ig6ny9lr875q3g7smnq33dcsmf2i5gd92";
|
||||
# }
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
@ -20,46 +19,6 @@ import yaml
|
|||
|
||||
PASSWORD_STORE = os.environ["PASSWORD_STORE_DIR"]
|
||||
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]:
|
||||
|
@ -75,28 +34,11 @@ def list_networks() -> list[str]:
|
|||
file = filename[:-4]
|
||||
path = os.path.join(SUBFOLDER, file)
|
||||
paths.append(path)
|
||||
paths.sort()
|
||||
return paths
|
||||
|
||||
|
||||
def format_wpa_supplicant_conf(conf: dict, indent: str = "") -> str:
|
||||
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 = {}
|
||||
networks: list[dict[str, str | list[str] | int]] = list()
|
||||
for path in list_networks():
|
||||
proc = subprocess.run(["pass", path], stdout=subprocess.PIPE)
|
||||
proc.check_returncode()
|
||||
|
@ -104,61 +46,24 @@ for path in list_networks():
|
|||
raw = proc.stdout.decode()
|
||||
split = raw.split("\n")
|
||||
|
||||
password = Password(path, split[0])
|
||||
password = split[0]
|
||||
data = yaml.safe_load("\n".join(split[1:])) or dict()
|
||||
# print(path, data) # DEBUG
|
||||
|
||||
# Helpers to prevent repetition
|
||||
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 any(map(lambda m: "PSK" in m.split("-"), data["key_mgmt"])):
|
||||
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"])):
|
||||
data["password"] = password
|
||||
assert "ssid" in data, f"{path}: Missing SSID"
|
||||
|
||||
# # 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)
|
||||
data.setdefault("disabled", 0)
|
||||
|
||||
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:
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue