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:
parent
81e5e70d27
commit
c37a709b01
|
@ -1,3 +0,0 @@
|
||||||
[Service]
|
|
||||||
ExecStart=
|
|
||||||
ExecStart=/usr/bin/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant.conf -i%I
|
|
|
@ -19,22 +19,6 @@
|
||||||
listen: systemd changed
|
listen: systemd changed
|
||||||
become: yes
|
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
|
- name: Warn about changed Panfrost config
|
||||||
debug:
|
debug:
|
||||||
msg: "The Panfrost display driver configuration was changed, but needs a reboot to be applied."
|
msg: "The Panfrost display driver configuration was changed, but needs a reboot to be applied."
|
||||||
|
|
|
@ -79,75 +79,6 @@
|
||||||
|
|
||||||
### STOPPED HERE ###
|
### 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
|
# Time synchronisation
|
||||||
|
|
||||||
- name: Mask systemd-timesyncd
|
- name: Mask systemd-timesyncd
|
||||||
|
|
|
@ -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=""
|
|
||||||
}
|
|
||||||
|
|
||||||
#}
|
|
|
@ -1,5 +1,14 @@
|
||||||
{ pkgs, ... }:
|
{ 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 ];
|
environment.systemPackages = with pkgs; [ wirelesstools ];
|
||||||
}
|
}
|
||||||
|
|
2
config/nix/modules/wireless/.gitignore
vendored
Normal file
2
config/nix/modules/wireless/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
networks.json
|
||||||
|
networks.env
|
161
config/nix/modules/wireless/import.py
Executable file
161
config/nix/modules/wireless/import.py
Executable 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)
|
Loading…
Reference in a new issue