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"
|
"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",
|
||||||
|
|
|
@ -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
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, ... }:
|
{ 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}";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
Loading…
Reference in a new issue