diff --git a/config/automatrop/roles/system/files/wpa_supplicant.service b/config/automatrop/roles/system/files/wpa_supplicant.service deleted file mode 100644 index a839d44..0000000 --- a/config/automatrop/roles/system/files/wpa_supplicant.service +++ /dev/null @@ -1,3 +0,0 @@ -[Service] -ExecStart= -ExecStart=/usr/bin/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant.conf -i%I diff --git a/config/automatrop/roles/system/handlers/main.yaml b/config/automatrop/roles/system/handlers/main.yaml index 595a97f..9bda647 100644 --- a/config/automatrop/roles/system/handlers/main.yaml +++ b/config/automatrop/roles/system/handlers/main.yaml @@ -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." diff --git a/config/automatrop/roles/system/tasks/main.yml b/config/automatrop/roles/system/tasks/main.yml index 6b78d93..7f9ce9f 100644 --- a/config/automatrop/roles/system/tasks/main.yml +++ b/config/automatrop/roles/system/tasks/main.yml @@ -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 diff --git a/config/automatrop/roles/system/templates/wpa_supplicant.conf.j2 b/config/automatrop/roles/system/templates/wpa_supplicant.conf.j2 deleted file mode 100644 index b380481..0000000 --- a/config/automatrop/roles/system/templates/wpa_supplicant.conf.j2 +++ /dev/null @@ -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="" -} - -#} diff --git a/config/nix/modules/wireless.nix b/config/nix/modules/wireless.nix index ef2a339..e68ba94 100644 --- a/config/nix/modules/wireless.nix +++ b/config/nix/modules/wireless.nix @@ -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 ]; } diff --git a/config/nix/modules/wireless/.gitignore b/config/nix/modules/wireless/.gitignore new file mode 100644 index 0000000..25887d8 --- /dev/null +++ b/config/nix/modules/wireless/.gitignore @@ -0,0 +1,2 @@ +networks.json +networks.env diff --git a/config/nix/modules/wireless/import.py b/config/nix/modules/wireless/import.py new file mode 100755 index 0000000..ea1ea40 --- /dev/null +++ b/config/nix/modules/wireless/import.py @@ -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)