nix: Wi-Fi configuration

It's, uh, a bit ugly.
But let's try to make it work for now, improve later.
Apparently my wpa_supplicant config file was visible for everyone
already, so that's not a regression :D
This commit is contained in:
Geoffrey Frogeye 2023-10-28 18:53:17 +02:00
parent 81e5e70d27
commit c37a709b01
Signed by: geoffrey
GPG key ID: C72403E7F82E6AD8
7 changed files with 173 additions and 191 deletions

View file

@ -1,3 +0,0 @@
[Service]
ExecStart=
ExecStart=/usr/bin/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant.conf -i%I

View file

@ -19,22 +19,6 @@
listen: systemd changed
become: yes
- name: Restart wpa_supplicant
systemd:
name: "wpa_supplicant@{{ item }}"
state: restarted
become: yes
loop: "{{ ansible_interfaces }}"
when: "item.startswith('wl')"
listen: wpa_supplicant changed
# Could probably use something better like
# listing /sys/class/ieee80211/*/device/net/
- name: Warn about changed Wi-Fi setup
debug:
msg: "The Wi-Fi configuration was changed, but not applied to let this playbook finish. A reboot is required."
listen: wifi setup changed
- name: Warn about changed Panfrost config
debug:
msg: "The Panfrost display driver configuration was changed, but needs a reboot to be applied."

View file

@ -79,75 +79,6 @@
### STOPPED HERE ###
- name: Configure wpa_supplicant
template:
src: wpa_supplicant.conf.j2
dest: /etc/wpa_supplicant/wpa_supplicant.conf
notify:
- etc changed
- wpa_supplicant changed
become: yes
tags:
- wificonf
- name: Prepare directory for wpa_supplicant service override
file:
path: /etc/systemd/system/wpa_supplicant@.service.d
state: directory
mode: "u=rwx,g=rx,o=rx"
become: yes
- name: Make wpa_supplicant use a common configuration file
copy:
src: wpa_supplicant.service
dest: /etc/systemd/system/wpa_supplicant@.service.d/override.conf
become: yes
notify:
- etc changed
- systemd changed
- wifi setup changed
- name: Disable wpa_supplicant for networkmanager
systemd:
name: wpa_supplicant
enabled: no
become: yes
notify:
- etc changed
- wifi setup changed
- name: Start/enable wpa_supplicant for interface
systemd:
name: "wpa_supplicant@{{ item }}"
enabled: yes
become: yes
notify:
- etc changed
- wifi setup changed
loop: "{{ ansible_interfaces }}"
when: "item.startswith('wl')"
# Could probably use something better like
# listing /sys/class/ieee80211/*/device/net/
- name: Uninstall networkmanager
pacman:
name: networkmanager
state: absent
extra_args: "--cascade --recursive"
when: arch_based
become: yes
notify:
- wifi setup changed
- name: Mask systemd-networkd
systemd:
name: systemd-networkd
state: stopped
enabled: no
masked: yes
become: yes
notify: etc changed
# Time synchronisation
- name: Mask systemd-timesyncd

View file

@ -1,102 +0,0 @@
# Giving configuration update rights to wpa_cli
ctrl_interface=/run/wpa_supplicant
ctrl_interface_group=wheel
update_config=1
# AP scanning
ap_scan=1
# ISO/IEC alpha2 country code in which the device is operating
country=NL
{% set password_store_path = lookup('env', 'PASSWORD_STORE_DIR') or ansible_user_dir + '/.password-store/' %}
{% set wifi_pass_paths = query('fileglob', password_store_path + 'wifi/*.gpg') %}
{% set names = wifi_pass_paths | map('regex_replace', '^.+/wifi/(.+).gpg$', '\\1') | sort%}
{% for name in names %}
{#
community.general.passwordstore doesn't support path with spaces in it,
so we're using a `ssid` attribute, which default to the names for SSIDs without space.
#}
{% set suffixes = lookup('community.general.passwordstore', 'wifi/' + name + ' subkey=suffixes') or [''] %}
{% set ssid = lookup('community.general.passwordstore', 'wifi/' + name + ' subkey=ssid') or name %}
{% set type = lookup('community.general.passwordstore', 'wifi/' + name + ' subkey=type') or 'wpa' %}
{% if type in ('wpa', 'wep', 'wpa-eap') %}
{% set pass = lookup('community.general.passwordstore', 'wifi/' + name) %}
{% else %}
{% set pass = 'Error, no pass for type ' + type %}
{% endif %}
# {{ name }}
{% for suffix in suffixes %}
network={
ssid="{{ ssid }}{{ suffix }}"
{% if type == 'wpa' %}
psk="{{ pass }}"
{% elif type == 'wep' %}
key_mgmt=NONE
wep_key0={{ pass }}
{% elif type == 'wpa-eap' %}
key_mgmt=WPA-EAP
eap={{ lookup('community.general.passwordstore', 'wifi/' + name + ' subkey=eap') }}
identity="{{ lookup('community.general.passwordstore', 'wifi/' + name + ' subkey=identity') }}"
password="{{ pass }}"
ca_cert="{{ lookup('community.general.passwordstore', 'wifi/' + name + ' subkey=ca_cert') }}"
altsubject_match="{{ lookup('community.general.passwordstore', 'wifi/' + name + ' subkey=altsubject_match') }}"
phase2="{{ lookup('community.general.passwordstore', 'wifi/' + name + ' subkey=phase2') }}"
{% elif type == 'open' %}
key_mgmt=NONE
{% else %}
# Error, unknown type: {{ type }}
{% endif %}
}
{% endfor %}
{% endfor %}
{# REFERENCES
# WPA
network={
ssid="WPA_SSID"
psk="XXXXXXXXXXXXXXXXXXXXXXXXXX"
}
# WEP
network={
ssid="WEP_SSID"
key_mgmt=NONE
wep_key0=FFFFFFFFFFFFFFFFFFFFFFFFFF
}
# Open
network={
ssid="OPEN_SSID"
key_mgmt=NONE
}
# eduroam password
network={
ssid="eduroam"
key_mgmt=WPA-EAP
eap=PEAP
identity="id@univ.tld"
password="hunter2"
}
# eduroam certificate
network={
ssid="eduroam"
key_mgmt=WPA-EAP
# pairwise=CCMP
pairwise=CCMP TKIP
group=CCMP TKIP
eap=TLS
ca_cert="/path/to/ca.pem"
identity="id@univ.tld"
domain_suffix_match="wifi.univ.tld"
client_cert="/path/to/cert.pem"
private_key="/path/to/key.pem"
private_key_passwd="hunter2"
phase2="auth="
#anonymous_identity=""
}
#}

View file

@ -1,5 +1,14 @@
{ pkgs, ... }:
{
networking.wireless.enable = true; # Enable wireless support via wpa_supplicant
# wireless support via wpa_supplicant
# TODO This doesn't change anything, at least in the VM
networking.wireless = {
enable = true;
networks = builtins.fromJSON ./wireless/networks.json; # If this file doesn't exist, run ./wireless/import.py
extraConfig = ''
country=NL
'';
interfaces = ["eth0"];
};
environment.systemPackages = with pkgs; [ wirelesstools ];
}

View file

@ -0,0 +1,2 @@
networks.json
networks.env

View file

@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
Exports Wi-Fi networks configuration stored in pass into a format readable by Nix.
"""
# TODO EAP ca_cert=/etc/ssl/... probably won't work. Example fix:
# builtins.fetchurl {
# url = "https://letsencrypt.org/certs/isrgrootx1.pem";
# sha256 = "sha256:1la36n2f31j9s03v847ig6ny9lr875q3g7smnq33dcsmf2i5gd92";
# }
import hashlib
import json
import os
import subprocess
import yaml
# passpy doesn't handle encoding properly, so doing this with calls
PASSWORD_STORE = os.path.expanduser("~/.password-store")
SUBFOLDER = "wifi"
SEPARATE_PASSWORDS = False
# TODO Find a way to make then env file available at whatever time it is needed
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 m.hexdigest().upper()
def val(self) -> str:
return 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]:
paths = []
pass_folder = os.path.join(PASSWORD_STORE, SUBFOLDER)
for filename in os.listdir(pass_folder):
if not filename.endswith(".gpg"):
continue
filepath = os.path.join(pass_folder, filename)
if not os.path.isfile(filepath):
continue
file = filename[:-4]
path = os.path.join(SUBFOLDER, file)
paths.append(path)
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 = {}
for path in list_networks():
proc = subprocess.run(["pass", path], stdout=subprocess.PIPE)
proc.check_returncode()
raw = proc.stdout.decode()
split = raw.split("\n")
password = Password(path, 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 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["pskRaw"] = 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:
networks[ssid + suffix] = network
with open("networks.json", "w") as fd:
json.dump(networks, fd, indent=4)
with open("networks.env", "w") as fd:
if SEPARATE_PASSWORDS:
for k, v in Password.vars().items():
print(f"{k}={v}", file=fd)