diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..c87a03b
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,10 @@
+
+[MASTER]
+# Add current directory to Python path so pylint can find local modules
+init-hook='import sys; sys.path.append(".")'
+ignore=.venv,venv,.git,__pycache__,.pytest_cache,build,dist
+
+[FORMAT]
+# Maximum line length
+max-line-length=120
+max-module-lines=1800
\ No newline at end of file
diff --git a/NetworkMgr/configuration.py b/NetworkMgr/configuration.py
index d0689a4..20e37da 100755
--- a/NetworkMgr/configuration.py
+++ b/NetworkMgr/configuration.py
@@ -1,12 +1,25 @@
#!/usr/bin/env python
-import gi
-import os
+"""The per-interface network configuration window.
+
+Two tabs, IPv4 and IPv6, each offering an automatic mode (DHCP or SLAAC) or
+a manual address, mask, gateway, DNS and search domain. Saving writes
+rc.conf through sysrc and /etc/resolv.conf directly, then applies the same
+settings to the running system through NetworkMgr.net_api.
+"""
+
import re
+from subprocess import DEVNULL, run
+
+import gi
gi.require_version('Gtk', '3.0')
+
+# gi.require_version() has to run before the typelib is loaded, so the
+# imports below cannot be hoisted above it.
+# pylint: disable=wrong-import-position
from gi.repository import Gtk, GLib
from NetworkMgr.net_api import (
- defaultcard,
+ default_card,
nics_list,
restart_card_network,
restart_routing_and_dhcp,
@@ -14,272 +27,300 @@
start_static_ipv6_network,
enable_slaac,
disable_slaac,
- wait_inet
+ wait_for_address
)
from NetworkMgr.query import get_interface_settings, get_interface_settings_ipv6
-from subprocess import run
-rcconf = open('/etc/rc.conf', 'r').read()
-if os.path.exists('/etc/rc.conf.local'):
- rcconflocal = open('/etc/rc.conf.local', 'r').read()
-else:
- rcconflocal = "None"
-
-class netCardConfigWindow(Gtk.Window):
+class NetCardConfigWindow(Gtk.Window):
+ """The configuration window for one network interface."""
def edit_ipv4_setting(self, widget):
+ """Switch the IPv4 tab between DHCP and manual entry.
+
+ Args:
+ widget (Gtk.RadioButton): the radio button that changed. Ignored
+ unless it is the one that became active.
+ """
if widget.get_active():
self.method = widget.get_label()
if self.method == "DHCP":
- self.ipInputAddressEntry.set_sensitive(False)
- self.ipInputMaskEntry.set_sensitive(False)
- self.ipInputGatewayEntry.set_sensitive(False)
- self.prymary_dnsEntry.set_sensitive(False)
- self.secondary_dnsEntry.set_sensitive(False)
+ self.ip_input_address_entry.set_sensitive(False)
+ self.ip_input_mask_entry.set_sensitive(False)
+ self.ip_input_gateway_entry.set_sensitive(False)
+ self.prymary_dns_entry.set_sensitive(False)
+ self.secondary_dns_entry.set_sensitive(False)
+ self.search_entry.set_sensitive(False)
else:
- self.ipInputAddressEntry.set_sensitive(True)
- self.ipInputMaskEntry.set_sensitive(True)
- self.ipInputGatewayEntry.set_sensitive(True)
- self.prymary_dnsEntry.set_sensitive(True)
- self.secondary_dnsEntry.set_sensitive(True)
- if self.method == self.currentSettings["Assignment Method"]:
- self.saveButton.set_sensitive(False)
+ self.ip_input_address_entry.set_sensitive(True)
+ self.ip_input_mask_entry.set_sensitive(True)
+ self.ip_input_gateway_entry.set_sensitive(True)
+ self.prymary_dns_entry.set_sensitive(True)
+ self.secondary_dns_entry.set_sensitive(True)
+ self.search_entry.set_sensitive(True)
+ if self.method == self.current_settings["Assignment Method"]:
+ self.save_button.set_sensitive(False)
else:
- self.saveButton.set_sensitive(True)
+ self.save_button.set_sensitive(self.settings_complete())
def edit_ipv6_setting(self, widget, value):
+ """Switch the IPv6 tab between SLAAC and manual entry.
+
+ Args:
+ widget (Gtk.RadioButton): the radio button that changed. Ignored
+ unless it is the one that became active.
+ value (str): the method the button stands for, "SLAAC" or
+ "Manual".
+ """
if widget.get_active():
self.method6 = value
# Check if GUI elements exist (may be called during init)
- if not hasattr(self, 'ipInputAddressEntry6'):
+ if not hasattr(self, 'ip_input_address_entry6'):
return
if value == "SLAAC":
- self.ipInputAddressEntry6.set_sensitive(False)
- self.ipInputMaskEntry6.set_sensitive(False)
- self.ipInputGatewayEntry6.set_sensitive(False)
- self.prymary_dnsEntry6.set_sensitive(False)
- self.searchEntry6.set_sensitive(False)
+ self.ip_input_address_entry6.set_sensitive(False)
+ self.ip_input_mask_entry6.set_sensitive(False)
+ self.ip_input_gateway_entry6.set_sensitive(False)
+ self.prymary_dns_entry6.set_sensitive(False)
+ self.search_entry6.set_sensitive(False)
else:
- self.ipInputAddressEntry6.set_sensitive(True)
- self.ipInputMaskEntry6.set_sensitive(True)
- self.ipInputGatewayEntry6.set_sensitive(True)
- self.prymary_dnsEntry6.set_sensitive(True)
- self.searchEntry6.set_sensitive(True)
+ self.ip_input_address_entry6.set_sensitive(True)
+ self.ip_input_mask_entry6.set_sensitive(True)
+ self.ip_input_gateway_entry6.set_sensitive(True)
+ self.prymary_dns_entry6.set_sensitive(True)
+ self.search_entry6.set_sensitive(True)
# Enable save button if method changed
- if self.method6 == self.currentSettings6["Assignment Method"]:
- self.saveButton.set_sensitive(False)
+ if self.method6 == self.current_settings6["Assignment Method"]:
+ self.save_button.set_sensitive(False)
else:
- self.saveButton.set_sensitive(True)
-
- def entry_trigger_save_button(self, widget, event):
- self.saveButton.set_sensitive(True)
+ self.save_button.set_sensitive(self.settings_complete())
+
+ def settings_complete(self):
+ """Report whether the entries hold enough to save.
+
+ Returns:
+ bool: False when the IPv4 method is Manual with no primary DNS
+ server, or the IPv6 method is Manual with no address.
+ """
+ if self.method == 'Manual' and not self.prymary_dns_entry.get_text().strip():
+ return False
+ if self.method6 == 'Manual' and not self.ip_input_address_entry6.get_text().strip():
+ return False
+ return True
+
+ def entry_trigger_save_button(self, _widget, _event):
+ """Enable Save as soon as the user edits any entry.
+
+ Args:
+ _widget (Gtk.Widget): the edited entry, unused.
+ _event (Gdk.Event): the key event, unused.
+ """
+ self.save_button.set_sensitive(self.settings_complete())
def __init__(self, selected_nic=None):
# Build Default Window
Gtk.Window.__init__(self, title="Network Configuration")
self.set_default_size(475, 400)
- self.NICS = nics_list()
- DEFAULT_NIC = selected_nic if selected_nic else defaultcard()
+ self.nics = nics_list()
+ default_nic = selected_nic if selected_nic else default_card()
# Build Tab 1 Content
# Interface Drop Down Combo Box
cell = Gtk.CellRendererText()
- interfaceComboBox = Gtk.ComboBox()
- interfaceComboBox.pack_start(cell, expand=True)
- interfaceComboBox.add_attribute(cell, 'text', 0)
+ interface_combo_box = Gtk.ComboBox()
+ interface_combo_box.pack_start(cell, expand=True)
+ interface_combo_box.add_attribute(cell, 'text', 0)
# Add interfaces to a ListStore
store = Gtk.ListStore(str)
- for nic in self.NICS:
+ for nic in self.nics:
store.append([nic])
- interfaceComboBox.set_model(store)
- interfaceComboBox.set_margin_top(15)
- interfaceComboBox.set_margin_end(30)
- if DEFAULT_NIC:
- active_index = self.NICS.index(f"{DEFAULT_NIC}")
- interfaceComboBox.set_active(active_index)
- self.currentSettings = get_interface_settings(DEFAULT_NIC)
- self.method = self.currentSettings["Assignment Method"]
+ interface_combo_box.set_model(store)
+ interface_combo_box.set_margin_top(15)
+ interface_combo_box.set_margin_end(30)
+ if default_nic:
+ active_index = self.nics.index(f"{default_nic}")
+ interface_combo_box.set_active(active_index)
+ self.current_settings = get_interface_settings(default_nic)
+ self.method = self.current_settings["Assignment Method"]
# IPv6 settings
- self.currentSettings6 = get_interface_settings_ipv6(DEFAULT_NIC)
- self.method6 = self.currentSettings6["Assignment Method"]
- interfaceComboBox.connect("changed", self.cbox_config_refresh, self.NICS)
+ self.current_settings6 = get_interface_settings_ipv6(default_nic)
+ self.method6 = self.current_settings6["Assignment Method"]
+ interface_combo_box.connect("changed", self.cbox_config_refresh)
# Build Label to sit in front of the ComboBox
- labelOne = Gtk.Label(label="Interface:")
- labelOne.set_margin_top(15)
- labelOne.set_margin_start(30)
+ label_one = Gtk.Label(label="Interface:")
+ label_one.set_margin_top(15)
+ label_one.set_margin_start(30)
# Add both objects to a single box, which will then be added to the grid
- interfaceBox = Gtk.Box(orientation=0, spacing=100)
- interfaceBox.pack_start(labelOne, False, False, 0)
- interfaceBox.pack_end(interfaceComboBox, True, True, 0)
+ interface_box = Gtk.Box(orientation=0, spacing=100)
+ interface_box.pack_start(label_one, False, False, 0)
+ interface_box.pack_end(interface_combo_box, True, True, 0)
# Add radio button to toggle DHCP or not
- self.version = self.currentSettings["Assignment Method"]
self.rb_dhcp4 = Gtk.RadioButton.new_with_label(None, "DHCP")
self.rb_dhcp4.set_margin_top(15)
self.rb_manual4 = Gtk.RadioButton.new_with_label_from_widget(
self.rb_dhcp4, "Manual")
self.rb_manual4.set_margin_top(15)
- if self.currentSettings["Assignment Method"] == "DHCP":
+ if self.current_settings["Assignment Method"] == "DHCP":
self.rb_dhcp4.set_active(True)
else:
self.rb_manual4.set_active(True)
self.rb_manual4.join_group(self.rb_dhcp4)
- radioButtonLabel = Gtk.Label(label="IPv4 Method:")
- radioButtonLabel.set_margin_top(15)
- radioButtonLabel.set_margin_start(30)
+ radio_button_label = Gtk.Label(label="IPv4 Method:")
+ radio_button_label.set_margin_top(15)
+ radio_button_label.set_margin_start(30)
- radioBox = Gtk.Box(orientation=0, spacing=50)
- radioBox.set_homogeneous(False)
- radioBox.pack_start(radioButtonLabel, False, False, 0)
- radioBox.pack_start(self.rb_dhcp4, True, False, 0)
- radioBox.pack_end(self.rb_manual4, True, True, 0)
+ radio_box = Gtk.Box(orientation=0, spacing=50)
+ radio_box.set_homogeneous(False)
+ radio_box.pack_start(radio_button_label, False, False, 0)
+ radio_box.pack_start(self.rb_dhcp4, True, False, 0)
+ radio_box.pack_end(self.rb_manual4, True, True, 0)
# Add Manual Address Field
- ipInputAddressLabel = Gtk.Label(label="Address")
- ipInputAddressLabel.set_margin_top(15)
+ ip_input_address_label = Gtk.Label(label="Address")
+ ip_input_address_label.set_margin_top(15)
- ipInputMaskLabel = Gtk.Label(label="Subnet Mask")
- ipInputMaskLabel.set_margin_top(15)
+ ip_input_mask_label = Gtk.Label(label="Subnet Mask")
+ ip_input_mask_label.set_margin_top(15)
- ipInputGatewayLabel = Gtk.Label(label="Gateway")
- ipInputGatewayLabel.set_margin_top(15)
+ ip_input_gateway_label = Gtk.Label(label="Gateway")
+ ip_input_gateway_label.set_margin_top(15)
- self.ipInputAddressEntry = Gtk.Entry()
- self.ipInputAddressEntry.set_margin_start(15)
- self.ipInputAddressEntry.set_text(self.currentSettings["Interface IP"])
- self.ipInputAddressEntry.connect("key-release-event", self.entry_trigger_save_button)
+ self.ip_input_address_entry = Gtk.Entry()
+ self.ip_input_address_entry.set_margin_start(15)
+ self.ip_input_address_entry.set_text(self.current_settings["Interface IP"])
+ self.ip_input_address_entry.connect("key-release-event", self.entry_trigger_save_button)
- self.ipInputMaskEntry = Gtk.Entry()
- self.ipInputMaskEntry.set_text(self.currentSettings["Interface Subnet Mask"])
- self.ipInputMaskEntry.connect("key-release-event", self.entry_trigger_save_button)
+ self.ip_input_mask_entry = Gtk.Entry()
+ self.ip_input_mask_entry.set_text(self.current_settings["Interface Subnet Mask"])
+ self.ip_input_mask_entry.connect("key-release-event", self.entry_trigger_save_button)
- self.ipInputGatewayEntry = Gtk.Entry()
- self.ipInputGatewayEntry.set_margin_end(15)
- self.ipInputGatewayEntry.set_text(self.currentSettings["Default Gateway"])
- self.ipInputGatewayEntry.connect("key-release-event", self.entry_trigger_save_button)
+ self.ip_input_gateway_entry = Gtk.Entry()
+ self.ip_input_gateway_entry.set_margin_end(15)
+ self.ip_input_gateway_entry.set_text(self.current_settings["Default Gateway"])
+ self.ip_input_gateway_entry.connect("key-release-event", self.entry_trigger_save_button)
- ipInputBox = Gtk.Box(orientation=0, spacing=0)
- ipInputBox.set_homogeneous(True)
- ipInputBox.pack_start(ipInputAddressLabel, False, False, 0)
- ipInputBox.pack_start(ipInputMaskLabel, False, False, 0)
- ipInputBox.pack_start(ipInputGatewayLabel, False, False, 0)
+ ip_input_box = Gtk.Box(orientation=0, spacing=0)
+ ip_input_box.set_homogeneous(True)
+ ip_input_box.pack_start(ip_input_address_label, False, False, 0)
+ ip_input_box.pack_start(ip_input_mask_label, False, False, 0)
+ ip_input_box.pack_start(ip_input_gateway_label, False, False, 0)
- ipEntryBox = Gtk.Box(orientation=0, spacing=30)
- ipEntryBox.pack_start(self.ipInputAddressEntry, False, False, 0)
- ipEntryBox.pack_start(self.ipInputMaskEntry, False, False, 0)
- ipEntryBox.pack_start(self.ipInputGatewayEntry, False, False, 0)
+ ip_entry_box = Gtk.Box(orientation=0, spacing=30)
+ ip_entry_box.pack_start(self.ip_input_address_entry, False, False, 0)
+ ip_entry_box.pack_start(self.ip_input_mask_entry, False, False, 0)
+ ip_entry_box.pack_start(self.ip_input_gateway_entry, False, False, 0)
# Add DNS Server Settings
- prymary_dns_Label = Gtk.Label(label="Primary DNS Servers: ")
- prymary_dns_Label.set_margin_top(15)
- prymary_dns_Label.set_margin_end(58)
- prymary_dns_Label.set_margin_start(30)
+ prymary_dns_label = Gtk.Label(label="Primary DNS Servers: ")
+ prymary_dns_label.set_margin_top(15)
+ prymary_dns_label.set_margin_end(58)
+ prymary_dns_label.set_margin_start(30)
- secondary_dns_Label = Gtk.Label(label="Secondary DNS Servers: ")
- secondary_dns_Label.set_margin_top(15)
- secondary_dns_Label.set_margin_end(58)
- secondary_dns_Label.set_margin_start(30)
+ secondary_dns_label = Gtk.Label(label="Secondary DNS Servers: ")
+ secondary_dns_label.set_margin_top(15)
+ secondary_dns_label.set_margin_end(58)
+ secondary_dns_label.set_margin_start(30)
- self.prymary_dnsEntry = Gtk.Entry()
- self.prymary_dnsEntry.set_margin_end(30)
- self.prymary_dnsEntry.set_text(self.currentSettings["DNS Server 1"])
- self.prymary_dnsEntry.connect("key-release-event", self.entry_trigger_save_button)
+ self.prymary_dns_entry = Gtk.Entry()
+ self.prymary_dns_entry.set_margin_end(30)
+ self.prymary_dns_entry.set_text(self.current_settings["DNS Server 1"])
+ self.prymary_dns_entry.connect("key-release-event", self.entry_trigger_save_button)
- self.secondary_dnsEntry = Gtk.Entry()
- self.secondary_dnsEntry.set_margin_end(30)
- self.secondary_dnsEntry.set_text(self.currentSettings["DNS Server 2"])
- self.secondary_dnsEntry.connect("key-release-event", self.entry_trigger_save_button)
+ self.secondary_dns_entry = Gtk.Entry()
+ self.secondary_dns_entry.set_margin_end(30)
+ self.secondary_dns_entry.set_text(self.current_settings["DNS Server 2"])
+ self.secondary_dns_entry.connect("key-release-event", self.entry_trigger_save_button)
- dnsEntryBox1 = Gtk.Box(orientation=0, spacing=0)
- dnsEntryBox1.pack_start(prymary_dns_Label, False, False, 0)
+ dns_entry_box1 = Gtk.Box(orientation=0, spacing=0)
+ dns_entry_box1.pack_start(prymary_dns_label, False, False, 0)
- dnsEntryBox1.pack_end(self.prymary_dnsEntry, True, True, 0)
+ dns_entry_box1.pack_end(self.prymary_dns_entry, True, True, 0)
- dnsEntryBox2 = Gtk.Box(orientation=0, spacing=0)
- dnsEntryBox2.pack_start(secondary_dns_Label, False, False, 0)
+ dns_entry_box2 = Gtk.Box(orientation=0, spacing=0)
+ dns_entry_box2.pack_start(secondary_dns_label, False, False, 0)
- dnsEntryBox2.pack_end(self.secondary_dnsEntry, True, True, 0)
+ dns_entry_box2.pack_end(self.secondary_dns_entry, True, True, 0)
# Add Search Domain Settings
- searchLabel = Gtk.Label(label="Search domains: ")
- searchLabel.set_margin_top(15)
- searchLabel.set_margin_end(30)
- searchLabel.set_margin_start(30)
-
- self.searchEntry = Gtk.Entry()
- self.searchEntry.set_margin_top(21)
- self.searchEntry.set_margin_end(30)
- self.searchEntry.set_margin_bottom(30)
- self.searchEntry.set_text(self.currentSettings["Search Domain"])
- self.searchEntry.connect("key-release-event", self.entry_trigger_save_button)
-
- searchBox = Gtk.Box(orientation=0, spacing=0)
- searchBox.pack_start(searchLabel, False, False, 0)
- searchBox.pack_end(self.searchEntry, True, True, 0)
+ search_label = Gtk.Label(label="Search domains: ")
+ search_label.set_margin_top(15)
+ search_label.set_margin_end(30)
+ search_label.set_margin_start(30)
+
+ self.search_entry = Gtk.Entry()
+ self.search_entry.set_margin_top(21)
+ self.search_entry.set_margin_end(30)
+ self.search_entry.set_margin_bottom(30)
+ self.search_entry.set_text(self.current_settings["Search Domain"])
+ self.search_entry.connect("key-release-event", self.entry_trigger_save_button)
+
+ search_box = Gtk.Box(orientation=0, spacing=0)
+ search_box.pack_start(search_label, False, False, 0)
+ search_box.pack_end(self.search_entry, True, True, 0)
self.rb_dhcp4.connect("toggled", self.edit_ipv4_setting)
self.rb_manual4.connect("toggled", self.edit_ipv4_setting)
- if self.currentSettings["Assignment Method"] == "DHCP":
- self.ipInputAddressEntry.set_sensitive(False)
- self.ipInputMaskEntry.set_sensitive(False)
- self.ipInputGatewayEntry.set_sensitive(False)
- self.prymary_dnsEntry.set_sensitive(False)
- self.secondary_dnsEntry.set_sensitive(False)
- self.searchEntry.set_sensitive(False)
-
- gridOne = Gtk.Grid()
- gridOne.set_column_homogeneous(True)
- gridOne.set_row_homogeneous(False)
- gridOne.set_column_spacing(5)
- gridOne.set_row_spacing(10)
- gridOne.attach(interfaceBox, 0, 0, 4, 1)
- gridOne.attach(radioBox, 0, 1, 4, 1)
- gridOne.attach(ipInputBox, 0, 2, 4, 1)
- gridOne.attach(ipEntryBox, 0, 3, 4, 1)
- gridOne.attach(dnsEntryBox1, 0, 4, 4, 1)
- gridOne.attach(dnsEntryBox2, 0, 5, 4, 1)
- gridOne.attach(searchBox, 0, 6, 4, 1)
+ if self.current_settings["Assignment Method"] == "DHCP":
+ self.ip_input_address_entry.set_sensitive(False)
+ self.ip_input_mask_entry.set_sensitive(False)
+ self.ip_input_gateway_entry.set_sensitive(False)
+ self.prymary_dns_entry.set_sensitive(False)
+ self.secondary_dns_entry.set_sensitive(False)
+ self.search_entry.set_sensitive(False)
+
+ grid_one = Gtk.Grid()
+ grid_one.set_column_homogeneous(True)
+ grid_one.set_row_homogeneous(False)
+ grid_one.set_column_spacing(5)
+ grid_one.set_row_spacing(10)
+ grid_one.attach(interface_box, 0, 0, 4, 1)
+ grid_one.attach(radio_box, 0, 1, 4, 1)
+ grid_one.attach(ip_input_box, 0, 2, 4, 1)
+ grid_one.attach(ip_entry_box, 0, 3, 4, 1)
+ grid_one.attach(dns_entry_box1, 0, 4, 4, 1)
+ grid_one.attach(dns_entry_box2, 0, 5, 4, 1)
+ grid_one.attach(search_box, 0, 6, 4, 1)
# Build Tab 2 Content
# Interface Drop Down Combo Box
cell6 = Gtk.CellRendererText()
- interfaceComboBox6 = Gtk.ComboBox()
- interfaceComboBox6.pack_start(cell6, expand=True)
- interfaceComboBox6.add_attribute(cell6, 'text', 0)
+ interface_combo_box6 = Gtk.ComboBox()
+ interface_combo_box6.pack_start(cell6, expand=True)
+ interface_combo_box6.add_attribute(cell6, 'text', 0)
# Add interfaces to a ListStore
store6 = Gtk.ListStore(str)
- for validinterface6 in self.NICS:
+ for validinterface6 in self.nics:
store6.append([validinterface6])
- interfaceComboBox6.set_model(store)
- interfaceComboBox6.set_margin_top(15)
- interfaceComboBox6.set_margin_end(30)
+ interface_combo_box6.set_model(store6)
+ interface_combo_box6.set_margin_top(15)
+ interface_combo_box6.set_margin_end(30)
- if DEFAULT_NIC:
- activeComboBoxObjectIndex6 = self.NICS.index(f"{DEFAULT_NIC}")
- interfaceComboBox6.set_active(activeComboBoxObjectIndex6)
- interfaceComboBox6.connect("changed", self.cbox_config_refresh)
+ if default_nic:
+ active_combo_box_object_index6 = self.nics.index(f"{default_nic}")
+ interface_combo_box6.set_active(active_combo_box_object_index6)
+ interface_combo_box6.connect("changed", self.cbox_config_refresh)
# Build Label to sit in front of the ComboBox
- labelOne6 = Gtk.Label(label="Interface:")
- labelOne6.set_margin_top(15)
- labelOne6.set_margin_start(30)
+ label_one6 = Gtk.Label(label="Interface:")
+ label_one6.set_margin_top(15)
+ label_one6.set_margin_start(30)
# Add both objects to a single box, which will then be added to the grid
- interfaceBox6 = Gtk.Box(orientation=0, spacing=100)
- interfaceBox6.pack_start(labelOne6, False, False, 0)
- interfaceBox6.pack_end(interfaceComboBox6, True, True, 0)
+ interface_box6 = Gtk.Box(orientation=0, spacing=100)
+ interface_box6.pack_start(label_one6, False, False, 0)
+ interface_box6.pack_end(interface_combo_box6, True, True, 0)
# Add radio button to toggle SLAAC or Manual
self.rb_slaac6 = Gtk.RadioButton.new_with_label(None, "SLAAC")
@@ -297,101 +338,101 @@ def __init__(self, selected_nic=None):
else:
self.rb_slaac6.set_active(True)
- radioButtonLabel6 = Gtk.Label(label="IPv6 Method:")
- radioButtonLabel6.set_margin_top(15)
- radioButtonLabel6.set_margin_start(30)
+ radio_button_label6 = Gtk.Label(label="IPv6 Method:")
+ radio_button_label6.set_margin_top(15)
+ radio_button_label6.set_margin_start(30)
- radioBox6 = Gtk.Box(orientation=0, spacing=50)
- radioBox6.set_homogeneous(False)
- radioBox6.pack_start(radioButtonLabel6, False, False, 0)
- radioBox6.pack_start(self.rb_slaac6, True, False, 0)
- radioBox6.pack_end(self.rb_manual6, True, True, 0)
+ radio_box6 = Gtk.Box(orientation=0, spacing=50)
+ radio_box6.set_homogeneous(False)
+ radio_box6.pack_start(radio_button_label6, False, False, 0)
+ radio_box6.pack_start(self.rb_slaac6, True, False, 0)
+ radio_box6.pack_end(self.rb_manual6, True, True, 0)
# Add Manual Address Field
- ipInputAddressLabel6 = Gtk.Label(label="Address")
- ipInputAddressLabel6.set_margin_top(15)
-
- ipInputMaskLabel6 = Gtk.Label(label="Prefix Length")
- ipInputMaskLabel6.set_margin_top(15)
-
- ipInputGatewayLabel6 = Gtk.Label(label="Gateway")
- ipInputGatewayLabel6.set_margin_top(15)
-
- self.ipInputAddressEntry6 = Gtk.Entry()
- self.ipInputAddressEntry6.set_margin_start(15)
- self.ipInputAddressEntry6.connect("key-release-event", self.entry_trigger_save_button)
- self.ipInputMaskEntry6 = Gtk.Entry()
- self.ipInputMaskEntry6.connect("key-release-event", self.entry_trigger_save_button)
- self.ipInputGatewayEntry6 = Gtk.Entry()
- self.ipInputGatewayEntry6.set_margin_end(15)
- self.ipInputGatewayEntry6.connect("key-release-event", self.entry_trigger_save_button)
-
- ipInputBox6 = Gtk.Box(orientation=0, spacing=0)
- ipInputBox6.set_homogeneous(True)
- ipInputBox6.pack_start(ipInputAddressLabel6, False, False, 0)
- ipInputBox6.pack_start(ipInputMaskLabel6, False, False, 0)
- ipInputBox6.pack_start(ipInputGatewayLabel6, False, False, 0)
-
- ipEntryBox6 = Gtk.Box(orientation=0, spacing=30)
- ipEntryBox6.pack_start(self.ipInputAddressEntry6, False, False, 0)
- ipEntryBox6.pack_start(self.ipInputMaskEntry6, False, False, 0)
- ipEntryBox6.pack_start(self.ipInputGatewayEntry6, False, False, 0)
+ ip_input_address_label6 = Gtk.Label(label="Address")
+ ip_input_address_label6.set_margin_top(15)
+
+ ip_input_mask_label6 = Gtk.Label(label="Prefix Length")
+ ip_input_mask_label6.set_margin_top(15)
+
+ ip_input_gateway_label6 = Gtk.Label(label="Gateway")
+ ip_input_gateway_label6.set_margin_top(15)
+
+ self.ip_input_address_entry6 = Gtk.Entry()
+ self.ip_input_address_entry6.set_margin_start(15)
+ self.ip_input_address_entry6.connect("key-release-event", self.entry_trigger_save_button)
+ self.ip_input_mask_entry6 = Gtk.Entry()
+ self.ip_input_mask_entry6.connect("key-release-event", self.entry_trigger_save_button)
+ self.ip_input_gateway_entry6 = Gtk.Entry()
+ self.ip_input_gateway_entry6.set_margin_end(15)
+ self.ip_input_gateway_entry6.connect("key-release-event", self.entry_trigger_save_button)
+
+ ip_input_box6 = Gtk.Box(orientation=0, spacing=0)
+ ip_input_box6.set_homogeneous(True)
+ ip_input_box6.pack_start(ip_input_address_label6, False, False, 0)
+ ip_input_box6.pack_start(ip_input_mask_label6, False, False, 0)
+ ip_input_box6.pack_start(ip_input_gateway_label6, False, False, 0)
+
+ ip_entry_box6 = Gtk.Box(orientation=0, spacing=30)
+ ip_entry_box6.pack_start(self.ip_input_address_entry6, False, False, 0)
+ ip_entry_box6.pack_start(self.ip_input_mask_entry6, False, False, 0)
+ ip_entry_box6.pack_start(self.ip_input_gateway_entry6, False, False, 0)
# Add DNS Server Settings
- prymary_dns_Label6 = Gtk.Label(label="Primary DNS Servers: ")
- prymary_dns_Label6.set_margin_top(15)
- prymary_dns_Label6.set_margin_end(58)
- prymary_dns_Label6.set_margin_start(30)
+ prymary_dns_label6 = Gtk.Label(label="Primary DNS Servers: ")
+ prymary_dns_label6.set_margin_top(15)
+ prymary_dns_label6.set_margin_end(58)
+ prymary_dns_label6.set_margin_start(30)
- secondary_dns_Label6 = Gtk.Label(label="Secondary DNS Servers: ")
- secondary_dns_Label6.set_margin_top(15)
- secondary_dns_Label6.set_margin_end(58)
- secondary_dns_Label6.set_margin_start(30)
+ secondary_dns_label6 = Gtk.Label(label="Secondary DNS Servers: ")
+ secondary_dns_label6.set_margin_top(15)
+ secondary_dns_label6.set_margin_end(58)
+ secondary_dns_label6.set_margin_start(30)
- self.prymary_dnsEntry6 = Gtk.Entry()
- self.prymary_dnsEntry6.set_margin_end(30)
- self.prymary_dnsEntry6.connect("key-release-event", self.entry_trigger_save_button)
+ self.prymary_dns_entry6 = Gtk.Entry()
+ self.prymary_dns_entry6.set_margin_end(30)
+ self.prymary_dns_entry6.connect("key-release-event", self.entry_trigger_save_button)
- dnsEntryBox6 = Gtk.Box(orientation=0, spacing=0)
- dnsEntryBox6.pack_start(prymary_dns_Label6, False, False, 0)
- dnsEntryBox6.pack_end(self.prymary_dnsEntry6, True, True, 0)
+ dns_entry_box6 = Gtk.Box(orientation=0, spacing=0)
+ dns_entry_box6.pack_start(prymary_dns_label6, False, False, 0)
+ dns_entry_box6.pack_end(self.prymary_dns_entry6, True, True, 0)
# Add Search Domain Settings
- searchLabel6 = Gtk.Label(label="Search domains: ")
- searchLabel6.set_margin_top(15)
- searchLabel6.set_margin_end(30)
- searchLabel6.set_margin_start(30)
+ search_label6 = Gtk.Label(label="Search domains: ")
+ search_label6.set_margin_top(15)
+ search_label6.set_margin_end(30)
+ search_label6.set_margin_start(30)
- self.searchEntry6 = Gtk.Entry()
- self.searchEntry6.set_margin_top(21)
- self.searchEntry6.set_margin_end(30)
- self.searchEntry6.set_margin_bottom(30)
- self.searchEntry6.connect("key-release-event", self.entry_trigger_save_button)
+ self.search_entry6 = Gtk.Entry()
+ self.search_entry6.set_margin_top(21)
+ self.search_entry6.set_margin_end(30)
+ self.search_entry6.set_margin_bottom(30)
+ self.search_entry6.connect("key-release-event", self.entry_trigger_save_button)
- searchBox6 = Gtk.Box(orientation=0, spacing=0)
- searchBox6.pack_start(searchLabel6, False, False, 0)
- searchBox6.pack_end(self.searchEntry6, True, True, 0)
+ search_box6 = Gtk.Box(orientation=0, spacing=0)
+ search_box6.pack_start(search_label6, False, False, 0)
+ search_box6.pack_end(self.search_entry6, True, True, 0)
# Set initial sensitivity based on current method (SLAAC = disabled, Manual = enabled)
- manual_enabled = (self.method6 == "Manual")
- self.ipInputAddressEntry6.set_sensitive(manual_enabled)
- self.ipInputMaskEntry6.set_sensitive(manual_enabled)
- self.ipInputGatewayEntry6.set_sensitive(manual_enabled)
- self.prymary_dnsEntry6.set_sensitive(manual_enabled)
- self.searchEntry6.set_sensitive(manual_enabled)
-
- gridOne6 = Gtk.Grid()
- gridOne6.set_column_homogeneous(True)
- gridOne6.set_row_homogeneous(False)
- gridOne6.set_column_spacing(5)
- gridOne6.set_row_spacing(10)
- gridOne6.attach(interfaceBox6, 0, 0, 4, 1)
- gridOne6.attach(radioBox6, 0, 1, 4, 1)
- gridOne6.attach(ipInputBox6, 0, 2, 4, 1)
- gridOne6.attach(ipEntryBox6, 0, 3, 4, 1)
- gridOne6.attach(dnsEntryBox6, 0, 4, 4, 1)
- gridOne6.attach(searchBox6, 0, 5, 4, 1)
+ manual_enabled = self.method6 == "Manual"
+ self.ip_input_address_entry6.set_sensitive(manual_enabled)
+ self.ip_input_mask_entry6.set_sensitive(manual_enabled)
+ self.ip_input_gateway_entry6.set_sensitive(manual_enabled)
+ self.prymary_dns_entry6.set_sensitive(manual_enabled)
+ self.search_entry6.set_sensitive(manual_enabled)
+
+ grid_one6 = Gtk.Grid()
+ grid_one6.set_column_homogeneous(True)
+ grid_one6.set_row_homogeneous(False)
+ grid_one6.set_column_spacing(5)
+ grid_one6.set_row_spacing(10)
+ grid_one6.attach(interface_box6, 0, 0, 4, 1)
+ grid_one6.attach(radio_box6, 0, 1, 4, 1)
+ grid_one6.attach(ip_input_box6, 0, 2, 4, 1)
+ grid_one6.attach(ip_entry_box6, 0, 3, 4, 1)
+ grid_one6.attach(dns_entry_box6, 0, 4, 4, 1)
+ grid_one6.attach(search_box6, 0, 5, 4, 1)
# Build Notebook
@@ -404,125 +445,139 @@ def __init__(self, selected_nic=None):
# nb.set_sensitive(False)
# Build Save & Cancel Buttons
- self.saveButton = Gtk.Button(label="Save")
- self.saveButton.set_margin_bottom(10)
- self.saveButton.set_margin_start(10)
- self.saveButton.connect("clicked", self.commit_pending_changes)
- self.saveButton.set_sensitive(False)
- cancelButton = Gtk.Button(label="Cancel")
- cancelButton.set_margin_bottom(10)
- cancelButton.connect("clicked", self.discard_pending_changes)
- buttonsWindow = Gtk.Box(orientation=0, spacing=10)
- buttonsWindow.pack_start(self.saveButton, False, False, 0)
- buttonsWindow.pack_start(cancelButton, False, False, 0)
+ self.save_button = Gtk.Button(label="Save")
+ self.save_button.set_margin_bottom(10)
+ self.save_button.set_margin_start(10)
+ self.save_button.connect("clicked", self.commit_pending_changes)
+ self.save_button.set_sensitive(False)
+ cancel_button = Gtk.Button(label="Cancel")
+ cancel_button.set_margin_bottom(10)
+ cancel_button.connect("clicked", self.discard_pending_changes)
+ buttons_window = Gtk.Box(orientation=0, spacing=10)
+ buttons_window.pack_start(self.save_button, False, False, 0)
+ buttons_window.pack_start(cancel_button, False, False, 0)
# Apply Tab 1 content and formatting to the notebook
- nb.append_page(gridOne)
- nb.set_tab_label_text(gridOne, "IPv4 Settings")
+ nb.append_page(grid_one)
+ nb.set_tab_label_text(grid_one, "IPv4 Settings")
# Apply Tab 2 content and formatting to the notebook
- nb.append_page(gridOne6)
- nb.set_tab_label_text(gridOne6, "IPv6 Settings")
+ nb.append_page(grid_one6)
+ nb.set_tab_label_text(grid_one6, "IPv6 Settings")
# Put all the widgets together into one window
- mainBox = Gtk.Box(orientation=1, spacing=0)
- mainBox.pack_start(nb, True, True, 0)
- mainBox.pack_end(buttonsWindow, False, False, 0)
- self.add(mainBox)
+ main_box = Gtk.Box(orientation=1, spacing=0)
+ main_box.pack_start(nb, True, True, 0)
+ main_box.pack_end(buttons_window, False, False, 0)
+ self.add(main_box)
# Used with the combo box to refresh the UI of tab 1 with active settings
# for the newly selected active interface.
- def cbox_config_refresh(self, widget, nics):
- # actions here need to refresh the values on the first two tabs.
- selected_nic = nics[widget.get_active()]
- self.currentSettings = get_interface_settings(selected_nic)
- self.currentSettings6 = get_interface_settings_ipv6(selected_nic)
+ def cbox_config_refresh(self, widget):
+ """Reload both tabs for the interface just chosen in the combo box.
+
+ Args:
+ widget (Gtk.ComboBox): the interface combo box.
+ """
+ selected_nic = self.nics[widget.get_active()]
+ self.current_settings = get_interface_settings(selected_nic)
+ self.current_settings6 = get_interface_settings_ipv6(selected_nic)
self.update_interface_settings()
self.update_interface_settings_ipv6()
def update_interface_settings(self):
- self.ipInputAddressEntry.set_text(self.currentSettings["Interface IP"])
- self.ipInputMaskEntry.set_text(self.currentSettings["Interface Subnet Mask"])
- self.ipInputGatewayEntry.set_text(self.currentSettings["Default Gateway"])
- self.prymary_dnsEntry.set_text(self.currentSettings["DNS Server 1"])
- self.secondary_dnsEntry.set_text(self.currentSettings["DNS Server 2"])
- self.searchEntry.set_text(self.currentSettings["Search Domain"])
- if self.currentSettings["Assignment Method"] == "DHCP":
+ """Fill the IPv4 tab's widgets from the current settings."""
+ self.ip_input_address_entry.set_text(self.current_settings["Interface IP"])
+ self.ip_input_mask_entry.set_text(self.current_settings["Interface Subnet Mask"])
+ self.ip_input_gateway_entry.set_text(self.current_settings["Default Gateway"])
+ self.prymary_dns_entry.set_text(self.current_settings["DNS Server 1"])
+ self.secondary_dns_entry.set_text(self.current_settings["DNS Server 2"])
+ self.search_entry.set_text(self.current_settings["Search Domain"])
+ if self.current_settings["Assignment Method"] == "DHCP":
self.rb_dhcp4.set_active(True)
else:
self.rb_manual4.set_active(True)
def update_interface_settings_ipv6(self):
- self.ipInputAddressEntry6.set_text(self.currentSettings6.get("Interface IPv6", ""))
- self.ipInputMaskEntry6.set_text(str(self.currentSettings6.get("Prefix Length", "64")))
- self.ipInputGatewayEntry6.set_text(self.currentSettings6.get("Default Gateway", ""))
- self.prymary_dnsEntry6.set_text(self.currentSettings6.get("DNS Server 1", ""))
- self.searchEntry6.set_text(self.currentSettings6.get("Search Domain", ""))
- self.method6 = self.currentSettings6.get("Assignment Method", "SLAAC")
+ """Fill the IPv6 tab's widgets from the current settings."""
+ self.ip_input_address_entry6.set_text(self.current_settings6.get("Interface IPv6", ""))
+ self.ip_input_mask_entry6.set_text(str(self.current_settings6.get("Prefix Length", "64")))
+ self.ip_input_gateway_entry6.set_text(self.current_settings6.get("Default Gateway", ""))
+ self.prymary_dns_entry6.set_text(self.current_settings6.get("DNS Server 1", ""))
+ self.search_entry6.set_text(self.current_settings6.get("Search Domain", ""))
+ self.method6 = self.current_settings6.get("Assignment Method", "SLAAC")
if self.method6 == "Manual":
self.rb_manual6.set_active(True)
- self.ipInputAddressEntry6.set_sensitive(True)
- self.ipInputMaskEntry6.set_sensitive(True)
- self.ipInputGatewayEntry6.set_sensitive(True)
- self.prymary_dnsEntry6.set_sensitive(True)
- self.searchEntry6.set_sensitive(True)
+ self.ip_input_address_entry6.set_sensitive(True)
+ self.ip_input_mask_entry6.set_sensitive(True)
+ self.ip_input_gateway_entry6.set_sensitive(True)
+ self.prymary_dns_entry6.set_sensitive(True)
+ self.search_entry6.set_sensitive(True)
else:
self.rb_slaac6.set_active(True)
- self.ipInputAddressEntry6.set_sensitive(False)
- self.ipInputMaskEntry6.set_sensitive(False)
- self.ipInputGatewayEntry6.set_sensitive(False)
- self.prymary_dnsEntry6.set_sensitive(False)
- self.searchEntry6.set_sensitive(False)
-
- def commit_pending_changes(self, widget):
+ self.ip_input_address_entry6.set_sensitive(False)
+ self.ip_input_mask_entry6.set_sensitive(False)
+ self.ip_input_gateway_entry6.set_sensitive(False)
+ self.prymary_dns_entry6.set_sensitive(False)
+ self.search_entry6.set_sensitive(False)
+
+ def commit_pending_changes(self, _widget):
+ """Hide the window and apply the settings on the GTK idle handler.
+
+ Args:
+ _widget (Gtk.Widget): the Save button, unused.
+ """
self.hide_window()
GLib.idle_add(self.update_system)
def update_system(self):
- nic = self.currentSettings["Active Interface"]
- inet = self.ipInputAddressEntry.get_text()
- netmask = self.ipInputMaskEntry.get_text()
- defaultrouter = self.ipInputGatewayEntry.get_text()
+ """Write the chosen IPv4 and IPv6 settings and apply them.
+
+ rc.conf is updated through sysrc, /etc/resolv.conf is rewritten for
+ a manual configuration, and the interface is restarted so the new
+ settings take effect straight away.
+ """
+ nic = self.current_settings["Active Interface"]
+ inet = self.ip_input_address_entry.get_text()
+ netmask = self.ip_input_mask_entry.get_text()
+ defaultrouter = self.ip_input_gateway_entry.get_text()
if self.method == 'Manual':
if 'wlan' in nic:
- ifconfig_nic = f'ifconfig_{nic}="WPA inet {inet} netmask {netmask}"\n'
+ ifconfig_value = f'WPA inet {inet} netmask {netmask}'
else:
- ifconfig_nic = f'ifconfig_{nic}="inet {inet} netmask {netmask}"\n'
- self.update_rc_conf(ifconfig_nic)
- defaultrouter_line = f'defaultrouter="{defaultrouter}"\n'
- self.update_rc_conf(defaultrouter_line)
+ ifconfig_value = f'inet {inet} netmask {netmask}'
+ self.set_rc_conf(f'ifconfig_{nic}', ifconfig_value)
+ self.set_rc_conf('defaultrouter', defaultrouter)
start_static_network(nic, inet, netmask)
- resolv_conf = open('/etc/resolv.conf', 'w')
- resolv_conf.writelines('# Generated by NetworkMgr\n')
- search = self.searchEntry.get_text()
- if search:
- search_line = f'search {search}\n'
- resolv_conf.writelines(search_line)
- dns1 = self.prymary_dnsEntry.get_text()
- nameserver1_line = f'nameserver {dns1}\n'
- resolv_conf.writelines(nameserver1_line)
- dns2 = self.secondary_dnsEntry.get_text()
- if dns2:
- nameserver2_line = f'nameserver {dns2}\n'
- resolv_conf.writelines(nameserver2_line)
- resolv_conf.close()
+ with open('/etc/resolv.conf', 'w', encoding='utf-8') as resolv_conf:
+ resolv_conf.writelines('# Generated by NetworkMgr\n')
+ search = self.search_entry.get_text()
+ if search:
+ search_line = f'search {search}\n'
+ resolv_conf.writelines(search_line)
+ dns1 = self.prymary_dns_entry.get_text()
+ nameserver1_line = f'nameserver {dns1}\n'
+ resolv_conf.writelines(nameserver1_line)
+ dns2 = self.secondary_dns_entry.get_text()
+ if dns2:
+ nameserver2_line = f'nameserver {dns2}\n'
+ resolv_conf.writelines(nameserver2_line)
else:
- if 'wlan' in nic:
- ifconfig_nic = f'ifconfig_{nic}="WPA DHCP"\n'
- else:
- ifconfig_nic = f'ifconfig_{nic}="DHCP"\n'
- self.update_rc_conf(ifconfig_nic)
+ self.set_rc_conf(f'ifconfig_{nic}',
+ 'WPA DHCP' if 'wlan' in nic else 'DHCP')
- rc_conf = open('/etc/rc.conf', 'r').read()
- for nic_search in self.NICS:
+ with open('/etc/rc.conf', 'r', encoding='utf-8') as rc_conf_file:
+ rc_conf = rc_conf_file.read()
+ for nic_search in self.nics:
if re.search(f'^ifconfig_{nic_search}=".*inet', rc_conf, re.MULTILINE):
break
else:
- defaultrouter_line = f'defaultrouter="{defaultrouter}"\n'
- self.remove_rc_conf_line(defaultrouter_line)
+ # Nothing static left, so dhclient takes the default
+ # route back.
+ self.remove_rc_conf_var('defaultrouter')
restart_card_network(nic)
# sometimes the inet address isn't available immediately after dhcp is enabled.
start_static_network(nic, inet, netmask)
- wait_inet(nic)
+ wait_for_address(nic)
restart_routing_and_dhcp(nic)
# Apply IPv6 configuration
@@ -532,18 +587,18 @@ def update_system(self):
def update_system_ipv6(self, nic):
"""Apply IPv6 configuration changes."""
- inet6 = self.ipInputAddressEntry6.get_text()
- prefixlen = self.ipInputMaskEntry6.get_text() or "64"
- gateway6 = self.ipInputGatewayEntry6.get_text()
- dns6 = self.prymary_dnsEntry6.get_text()
+ inet6 = self.ip_input_address_entry6.get_text()
+ prefixlen = self.ip_input_mask_entry6.get_text() or "64"
+ gateway6 = self.ip_input_gateway_entry6.get_text()
+ dns6 = self.prymary_dns_entry6.get_text()
if self.method6 == 'Manual':
# Static IPv6 configuration
- ifconfig_ipv6 = f'ifconfig_{nic}_ipv6="inet6 {inet6} prefixlen {prefixlen}"'
- self.update_rc_conf(ifconfig_ipv6)
+ self.set_rc_conf(f'ifconfig_{nic}_ipv6',
+ f'inet6 {inet6} prefixlen {prefixlen}')
# Disable rtsold for static configuration
- self.update_rc_conf('rtsold_enable="NO"')
+ self.set_rc_conf('rtsold_enable', 'NO')
# Apply the static IPv6 address
if inet6:
@@ -558,82 +613,101 @@ def update_system_ipv6(self, nic):
else:
gateway6_full = gateway6
# Save with interface suffix in rc.conf for persistence
- self.update_rc_conf(f'ipv6_defaultrouter="{gateway6_full}"')
+ self.set_rc_conf('ipv6_defaultrouter', gateway6_full)
# Apply gateway immediately
- run('route delete -inet6 default 2>/dev/null', shell=True)
- run(f'route add -inet6 default {gateway6_full}', shell=True)
+ run(['route', 'delete', '-inet6', 'default'],
+ stderr=DEVNULL, check=False)
+ run(['route', 'add', '-inet6', 'default', gateway6_full],
+ check=False)
# Add IPv6 DNS to resolv.conf if provided
if dns6:
self.add_ipv6_dns(dns6)
else:
# SLAAC configuration
- ifconfig_ipv6 = f'ifconfig_{nic}_ipv6="inet6 accept_rtadv"'
- self.update_rc_conf(ifconfig_ipv6)
+ self.set_rc_conf(f'ifconfig_{nic}_ipv6', 'inet6 accept_rtadv')
# Enable rtsold for SLAAC
- self.update_rc_conf('rtsold_enable="YES"')
+ self.set_rc_conf('rtsold_enable', 'YES')
- # Remove static IPv6 gateway if switching to SLAAC
- try:
- self.remove_rc_conf_var('ipv6_defaultrouter')
- except Exception:
- pass # Variable may not exist
+ # SLAAC supplies the gateway. sysrc -x on an unset variable
+ # is harmless.
+ self.remove_rc_conf_var('ipv6_defaultrouter')
# Enable SLAAC
enable_slaac(nic)
def add_ipv6_dns(self, dns6):
- """Add IPv6 DNS server to resolv.conf without removing existing entries."""
+ """Add an IPv6 nameserver to resolv.conf, keeping the existing ones.
+
+ Args:
+ dns6 (str): the IPv6 nameserver address to add. Nothing is
+ written if resolv.conf already lists it.
+ """
resolv_path = '/etc/resolv.conf'
- try:
- with open(resolv_path, 'r') as f:
- content = f.read()
- # Check if this IPv6 DNS is already present
- if f'nameserver {dns6}' not in content:
- with open(resolv_path, 'a') as f:
- f.write(f'nameserver {dns6}\n')
- except Exception:
- pass # resolv.conf may be managed by dhclient
+ with open(resolv_path, 'r', encoding='utf-8') as resolv_file:
+ content = resolv_file.read()
+ if f'nameserver {dns6}' not in content:
+ with open(resolv_path, 'a', encoding='utf-8') as resolv_file:
+ resolv_file.write(f'nameserver {dns6}\n')
def remove_rc_conf_var(self, varname):
- """Remove a variable from rc.conf using sysrc."""
- run(f'sysrc -x {varname}', shell=True)
+ """Remove a variable from rc.conf using sysrc.
+
+ Args:
+ varname (str): the rc.conf variable to unset.
+ """
+ run(['sysrc', '-x', varname], check=False)
def hide_window(self):
+ """Hide the window.
+
+ Returns:
+ bool: False, so GLib.idle_add does not call this again.
+ """
self.hide()
return False
- def discard_pending_changes(self, widget):
+ def discard_pending_changes(self, _widget):
+ """Close the window without applying anything.
+
+ Args:
+ _widget (Gtk.Widget): the Cancel button, unused.
+ """
self.destroy()
- def update_rc_conf(self, line):
- run(f'sysrc {line}', shell=True)
-
- def remove_rc_conf_line(self, line):
- try:
- with open('/etc/rc.conf', "r+") as rc_conf:
- lines = rc_conf.readlines()
- if line in lines:
- rc_conf.seek(0)
- idx = lines.index(line)
- lines.pop(idx)
- rc_conf.truncate()
- rc_conf.writelines(lines)
- except (ValueError, FileNotFoundError):
- pass # Line doesn't exist, nothing to remove
-
-
-def network_card_configuration(default_int):
- win = netCardConfigWindow(default_int)
+ def set_rc_conf(self, name, value):
+ """Set one rc.conf variable through sysrc.
+
+ The name and the value are passed as a single argument rather than
+ built into a shell command, so a value holding spaces needs no
+ quoting and a value holding a quote or a semicolon cannot run
+ anything. sysrc adds the quotes when it writes the file.
+
+ Args:
+ name (str): the rc.conf variable, for instance ifconfig_em0.
+ value (str): its value, for instance "inet 10.0.0.2 netmask
+ 255.255.255.0". Spaces are fine.
+ """
+ run(['sysrc', f'{name}={value}'], check=False)
+
+
+def open_configuration(default_int):
+ """Open the configuration window for one interface and run GTK.
+
+ Args:
+ default_int (str): the interface to select when the window opens.
+ """
+ win = NetCardConfigWindow(default_int)
win.connect("destroy", Gtk.main_quit)
win.show_all()
win.set_keep_above(True)
Gtk.main()
-def network_card_configuration_window():
- win = netCardConfigWindow()
+def open_default_configuration():
+ """Open the configuration window on the default interface and run GTK."""
+ win = NetCardConfigWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
win.set_keep_above(True)
diff --git a/NetworkMgr/net_api.py b/NetworkMgr/net_api.py
index b4416c6..081aa9d 100755
--- a/NetworkMgr/net_api.py
+++ b/NetworkMgr/net_api.py
@@ -1,9 +1,26 @@
#!/usr/bin/env python
-from subprocess import Popen, PIPE, run, check_output
+"""Network operations backing the tray icon and the configuration window.
+
+Everything that reads or changes the system's network state lives here:
+interface enumeration and status, the wpa_supplicant control-socket layer
+(scanning, saved networks, connection state), the wpa_supplicant.conf
+readers and writers, and the wired and wireless bring-up commands. Nothing
+in this module touches GTK.
+"""
+
+from concurrent.futures import ThreadPoolExecutor
+from subprocess import (
+ DEVNULL,
+ CalledProcessError,
+ TimeoutExpired,
+ run,
+ check_output
+)
import os
import re
-from time import sleep
+from string import hexdigits
+from time import monotonic, sleep
# EAP methods supported for enterprise WPA
@@ -12,144 +29,160 @@
# Phase 2 (inner) authentication methods
PHASE2_METHODS = ['MSCHAPV2', 'GTC', 'PAP', 'CHAP', 'MD5']
-# Default CA certificate path on FreeBSD/GhostBSD
-DEFAULT_CA_CERT = '/etc/ssl/certs/ca-root-nss.crt'
+# Matches a dotted-quad IPv4 address in ifconfig output.
+IP_REGEX = r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'
+# Threads for one refresh pass. They wait on subprocesses, not the CPU.
+_REFRESH_WORKERS = 6
-def card_online(netcard):
- lan = Popen('ifconfig ' + netcard, shell=True, stdout=PIPE,
- universal_newlines=True)
- return 'inet ' in lan.stdout.read()
+def _run(*args, check=False):
+ """
+ Run a command without a shell and return the finished process.
-def defaultcard():
- cmd = "netstat -rn | grep default"
- nics = Popen(cmd, shell=True, stdout=PIPE, universal_newlines=True)
- device = nics.stdout.readlines()
- if len(device) == 0:
- return None
- else:
- return list(filter(None, device[0].rstrip().split()))[3]
+ Args:
+ *args (str): the program and its arguments, one word per argument.
+ check (bool): raise CalledProcessError on a non-zero exit. Left False
+ where a non-zero exit is a normal answer, such as asking about an
+ interface that is not there.
+ Returns:
+ subprocess.CompletedProcess: with stdout and stderr captured as text.
+ """
+ return run(args, capture_output=True, text=True, check=check)
-def ifWlanDisable(wificard):
- cmd = "ifconfig %s list scan" % wificard
- nics = Popen(cmd, shell=True, stdout=PIPE, universal_newlines=True)
- return not nics.stdout.read()
+def _ifconfig(card):
+ """
+ Return the ifconfig output for one interface.
-def ifStatue(wificard):
- cmd = "ifconfig %s" % wificard
- wl = Popen(cmd, shell=True, stdout=PIPE, universal_newlines=True)
- wlout = wl.stdout.read()
- return "associated" in wlout
+ Args:
+ card (str): interface name, for instance em0 or wlan0.
+ Returns:
+ str: everything ifconfig printed, or an empty string when the
+ interface does not exist.
+ """
+ return _run('ifconfig', card).stdout
-def get_ssid(wificard):
- wlan = Popen('ifconfig %s | grep ssid' % wificard,
- shell=True, stdout=PIPE, universal_newlines=True)
- # If there are quotation marks in the string, use that as a separator,
- # otherwise use the default whitespace. This is to handle ssid strings
- # with spaces in them. These ssid strings will be double-quoted by ifconfig
- temp = wlan.stdout.readlines()[0].rstrip()
- if '"' in temp:
- out = temp.split('"')[1]
- else:
- out = temp.split()[1]
- return out
+def ifconfig_snapshot():
+ """Read every interface's ifconfig output in a single call.
-def nics_list():
- not_nics_regex = r"(enc|lo|fwe|fwip|tap|plip|pfsync|pflog|ipfw|tun|sl|" \
- r"faith|ppp|bridge|wg)[0-9]+(\s*)|vm-[a-z]+(\s*)"
- nics = Popen(
- 'ifconfig -l',
- shell=True,
- stdout=PIPE,
- universal_newlines=True
- ).stdout.read().strip()
- return sorted(re.sub(not_nics_regex, '', nics).strip().split())
+ One call, so every interface's state comes from the same instant.
+ Returns:
+ dict[str, str]: interface name mapped to its block of output.
+ """
+ return {
+ match.group(1): match.group(0)
+ for match in re.finditer(r'^(\w+):.*?(?=^\w+:|\Z)',
+ _run('ifconfig').stdout, re.M | re.S)
+ }
-def ifcardconnected(netcard):
- wifi = Popen('ifconfig ' + netcard, shell=True, stdout=PIPE,
- universal_newlines=True)
- return 'status: active' in wifi.stdout.read()
+def card_has_address(ifconfig_text):
+ """
+ Report whether an interface has an IPv4 address.
-def barpercent(sn):
- sig = int(sn.partition(':')[0])
- noise = int(sn.partition(':')[2])
- return int((sig - noise) * 4)
+ Args:
+ ifconfig_text (str): that interface's block from ifconfig_snapshot().
+
+ Returns:
+ bool: True when ifconfig lists an inet address on the interface.
+ """
+ return 'inet ' in ifconfig_text
-def is_enterprise_network(caps_string):
+def default_card():
"""
- Detect if a network uses WPA-Enterprise (802.1X/EAP) authentication.
+ Return the interface that carries the default route.
- FreeBSD ifconfig scan doesn't explicitly distinguish PSK from EAP.
- We use heuristics based on capability flags:
+ The routing table is filtered here rather than piped through grep, which
+ is what used to require a shell.
+
+ Returns:
+ str or None: the interface name, or None when there is no default
+ route at all.
+ """
+ for line in _run('netstat', '-rn').stdout.splitlines():
+ if line.startswith('default'):
+ fields = line.split()
+ if len(fields) > 3:
+ return fields[3]
+ return None
- 1. Explicit EAP indicators (if present)
- 2. RSN without WPS AND without typical home router flags (HTCAP, VHTCAP, ATH)
- suggests a minimal enterprise AP configuration
- Note: This is imperfect - some networks may be misdetected.
+def _is_disabled(ifconfig_text):
"""
- caps_upper = caps_string.upper()
+ Report whether an interface has been administratively downed.
- # Explicit enterprise indicators (highest confidence)
- enterprise_indicators = ['EAP', '802.1X', 'WPA2-EAP', 'WPA-EAP', 'RSN-EAP']
- for indicator in enterprise_indicators:
- if indicator in caps_upper:
- return True
+ The UP flag is the only reliable evidence. An empty scan list does not
+ mean the card is off (net80211 flushes the cache on INIT), and a wired
+ `status:` line describes the link, not the interface.
- # Heuristic: RSN without WPS and without typical consumer router features
- # Enterprise APs often have minimal beacon flags
- has_rsn = 'RSN' in caps_upper
- has_wps = 'WPS' in caps_upper
- has_consumer_features = any(f in caps_upper for f in ['HTCAP', 'VHTCAP', 'ATH', 'WME'])
+ Args:
+ ifconfig_text (str): that interface's block from ifconfig_snapshot().
- # Only flag as enterprise if: has RSN, no WPS, and no typical consumer features
- if has_rsn and not has_wps and not has_consumer_features:
- return True
+ Returns:
+ bool: True when the UP flag is absent. False when the interface has
+ no flags line at all, which means it is not there to be disabled.
+ """
+ flags = re.search(r'flags=\w+<([^>]*)>', ifconfig_text)
+ if flags is None:
+ return False
+ return 'UP' not in flags.group(1).split(',')
- return False
+def nics_list(snapshot=None):
+ """
+ List the interfaces networkmgr manages.
+
+ Args:
+ snapshot (dict or None): an ifconfig_snapshot() result to take the
+ names from. Left None by callers that have no snapshot to hand,
+ which then costs one `ifconfig -l`.
-def get_security_type(caps_string):
+ Returns:
+ list[str]: sorted interface names, with the virtual and tunnel
+ devices such as lo, bridge, tun and wg removed.
"""
- Determine the security type of a wireless network.
- Returns: 'OPEN', 'WEP', 'WPA-PSK', 'WPA2-PSK', 'WPA-EAP', 'WPA2-EAP'
+ not_nics_regex = r"(enc|lo|fwe|fwip|tap|plip|pfsync|pflog|ipfw|tun|sl|" \
+ r"faith|ppp|bridge|wg)[0-9]+(\s*)|vm-[a-z]+(\s*)"
+ if snapshot is None:
+ nics = _run('ifconfig', '-l').stdout.strip()
+ else:
+ nics = ' '.join(snapshot)
+ return sorted(re.sub(not_nics_regex, '', nics).strip().split())
+
+
+def card_has_carrier(ifconfig_text):
"""
- caps_upper = caps_string.upper()
- if is_enterprise_network(caps_string):
- if 'RSN' in caps_upper or 'WPA2' in caps_upper:
- return 'WPA2-EAP'
- return 'WPA-EAP'
- elif 'RSN' in caps_upper:
- return 'WPA2-PSK'
- elif 'WPA' in caps_upper:
- return 'WPA-PSK'
- elif 'WEP' in caps_upper or 'PRIVACY' in caps_upper:
- return 'WEP'
- return 'OPEN'
+ Report whether a wired interface has a cable in it.
+ Args:
+ ifconfig_text (str): that interface's block from ifconfig_snapshot().
-def validate_certificate(cert_path):
+ Returns:
+ bool: True when ifconfig reports `status: active`, which is carrier
+ and not the same question as whether the card has an address.
"""
- Validate that a certificate file exists and is readable.
- Returns tuple (is_valid, error_message).
+ return 'status: active' in ifconfig_text
+
+
+def bar_percent(level, noise):
+ """
+ Turn a signal and noise pair into the percentage the tray shows.
+
+ Args:
+ level (int): received signal strength in dBm, a negative number.
+ noise (int): the radio's noise floor in dBm, also negative.
+
+ Returns:
+ int: a percentage, deliberately not clamped, matching the old
+ ifconfig S:N arithmetic so the icons pick the same buckets.
"""
- if not cert_path:
- return False, "No certificate path provided"
- if not os.path.exists(cert_path):
- return False, f"Certificate file not found: {cert_path}"
- if not os.path.isfile(cert_path):
- return False, f"Not a file: {cert_path}"
- if not os.access(cert_path, os.R_OK):
- return False, f"Certificate file not readable: {cert_path}"
- return True, None
+ return int((level - noise) * 4)
def get_system_ca_certificates():
@@ -168,398 +201,1156 @@ def get_system_ca_certificates():
return available
-def network_service_state():
- return False
+def connected_signal(wifi_card):
+ """
+ Signal percentage of the access point this card is joined to.
+ Reads the one BSS the card is on, so this does not depend on the scan
+ table.
-def networkdictionary():
- nlist = nics_list()
- maindictionary = {
- 'service': network_service_state(),
- 'default': defaultcard()
- }
- cards = {}
- for card in nlist:
- if 'wlan' in card:
- scanv = f"ifconfig {card} list scan | grep -va BSSID"
- wifi = Popen(scanv, shell=True, stdout=PIPE,
- universal_newlines=True)
- connectioninfo = {}
- for line in wifi.stdout:
- # don't sort empty ssid
- # Window, MacOS and Linux does not show does
- if line.startswith(" " * 5):
- continue
- ssid = line[:33].strip()
- info = line[:83][33:].strip().split()
- percentage = barpercent(info[3])
- # if ssid exist and percentage is higher keep it
- # else add the new one if percentage is higher
- if ssid in connectioninfo:
- if connectioninfo[ssid][4] > percentage:
- continue
- info[3] = percentage
- info.insert(0, ssid)
- # append left over
- caps_string = line[83:].strip()
- info.append(caps_string)
- # Add security type info (index 7)
- info.append(get_security_type(caps_string))
- # Add enterprise flag (index 8)
- info.append(is_enterprise_network(caps_string))
- connectioninfo[ssid] = info
- if ifWlanDisable(card):
- connectionstat = {
- "connection": "Disabled",
- "ssid": None,
- }
- elif not ifStatue(card):
- connectionstat = {
- "connection": "Disconnected",
- "ssid": None,
+ Args:
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ int or None: the percentage, or None when the card is not joined to
+ anything or the driver reports no level.
+ """
+ bss = _parse_key_values(wpa_cli(wifi_card, 'bss', 'current') or '')
+ if bss.get('level') and bss.get('noise'):
+ return bar_percent(int(bss['level']), int(bss['noise']))
+ return None
+
+
+def _wireless_state(ifconfig_text, status):
+ """
+ Describe what a wireless interface is doing.
+
+ Connected means wpa_state=COMPLETED, the handshake finished. ifconfig's
+ `associated` appears about three seconds earlier, before the card can
+ pass traffic.
+
+ Args:
+ ifconfig_text (str): that interface's block from ifconfig_snapshot().
+ status (dict): the wpa_status() result for the same interface, passed
+ in so the daemon is asked once per refresh.
+
+ Returns:
+ dict: with 'connection' set to Disabled, Disconnected or Connected,
+ and 'ssid' set only when connected.
+ """
+ if _is_disabled(ifconfig_text):
+ return {'connection': 'Disabled', 'ssid': None}
+ if status.get('wpa_state') == 'COMPLETED':
+ return {'connection': 'Connected', 'ssid': status.get('ssid')}
+ return {'connection': 'Disconnected', 'ssid': None}
+
+
+def _wired_state(ifconfig_text):
+ """
+ Describe what a wired interface is doing.
+
+ Args:
+ ifconfig_text (str): that interface's block from ifconfig_snapshot().
+
+ Returns:
+ dict: with 'connection' set to Disabled when we downed the interface,
+ Unplug when there is no cable, Connected when it holds an
+ address, and Disconnected when it has carrier but no address.
+ """
+ if _is_disabled(ifconfig_text):
+ return {'connection': 'Disabled'}
+ if not card_has_carrier(ifconfig_text):
+ return {'connection': 'Unplug'}
+ if card_has_address(ifconfig_text):
+ return {'connection': 'Connected'}
+ return {'connection': 'Disconnected'}
+
+
+def network_dictionary():
+ """
+ Collect everything the tray menu draws itself from.
+
+ Every command runs here once and the results are handed down, rather
+ than each reader fetching its own.
+
+ Returns:
+ dict: 'default' names the interface holding the default route.
+ 'cards' maps each interface to 'state' (see _wireless_state and
+ _wired_state), 'signal' for signal, and the raw 'ifconfig' and
+ 'status' the state came from. No networks in range: that is
+ scan_networks(), called when the submenu opens.
+ """
+ # Two waves: netstat runs alongside ifconfig, but the daemon queries
+ # cannot start until ifconfig has said which cards are wireless.
+ with ThreadPoolExecutor(max_workers=_REFRESH_WORKERS) as pool:
+ default = pool.submit(default_card)
+ snapshot = ifconfig_snapshot()
+
+ interfaces = nics_list(snapshot)
+ wireless = [c for c in interfaces if 'wlan' in c]
+ statuses = {c: pool.submit(wpa_status, c) for c in wireless}
+ signals = {c: pool.submit(connected_signal, c) for c in wireless}
+
+ cards = {}
+ for card in interfaces:
+ ifconfig_text = snapshot.get(card, '')
+ if card in statuses:
+ status = statuses[card].result()
+ state = _wireless_state(ifconfig_text, status)
+ cards[card] = {
+ 'state': state,
+ 'signal': signals[card].result(),
+ 'ifconfig': ifconfig_text,
+ 'status': status,
}
else:
- ssid = get_ssid(card)
- connectionstat = {
- "connection": "Connected",
- "ssid": ssid,
+ cards[card] = {
+ 'state': _wired_state(ifconfig_text),
+ 'signal': None,
+ 'ifconfig': ifconfig_text,
+ 'status': {},
}
- seconddictionary = {
- 'state': connectionstat,
- 'info': connectioninfo
- }
- else:
- if card_online(card):
- connectionstat = {"connection": "Connected"}
- elif ifcardconnected(card):
- connectionstat = {"connection": "Disconnected"}
- else:
- connectionstat = {"connection": "Unplug"}
- seconddictionary = {'state': connectionstat, 'info': None}
- cards[card] = seconddictionary
- maindictionary['cards'] = cards
- return maindictionary
+ return {'default': default.result(), 'cards': cards}
+
+
+def _inet_line(ifconfig_text):
+ """
+ Return an interface's IPv4 address line as ifconfig prints it.
+
+ Args:
+ ifconfig_text (str): that interface's block from ifconfig_snapshot().
+
+ Returns:
+ str: the whole `inet ...` line, or an empty string when the interface
+ has no IPv4 address.
+ """
+ for line in ifconfig_text.splitlines():
+ if line.strip().startswith('inet '):
+ return line.strip()
+ return ''
+
+def tray_state():
+ """
+ Read only what the tray icon and its tooltip show.
+
+ Deliberately narrower than network_dictionary(): one interface, up or
+ not, and its signal from `bss current` rather than the scan table.
-def connectionStatus(card: str, network_info: dict) -> str:
+ Returns:
+ dict: 'card' naming the interface holding the default route or None,
+ 'kind' being 'wifi', 'wire' or None, 'connection' as
+ _wireless_state or _wired_state reports it, 'ssid' and 'signal'
+ filled in for a connected wireless card, and 'tooltip' ready to
+ display.
+ """
+ card = default_card()
if card is None:
- netstate = "Network card is not enabled"
- elif 'wlan' in card:
- if not ifWlanDisable(card) and ifStatue(card):
- cmd1 = "ifconfig %s | grep ssid" % card
- cmd2 = "ifconfig %s | grep 'inet '" % card
- out1 = Popen(cmd1, shell=True, stdout=PIPE, universal_newlines=True)
- out2 = Popen(cmd2, shell=True, stdout=PIPE, universal_newlines=True)
- ssid_info = out1.stdout.read().strip()
- inet_info = out2.stdout.read().strip()
- ssid = network_info['cards'][card]['state']["ssid"]
- percentage = network_info['cards'][card]['info'][ssid][4]
- netstate = f"Signal Strength: {percentage}% \n{ssid_info} \n{subnetHexToDec(inet_info)}"
- else:
- netstate = "WiFi %s not connected" % card
+ return {'card': None, 'kind': None, 'connection': 'Disconnected',
+ 'ssid': None, 'signal': None,
+ 'tooltip': "Network card is not enabled"}
+
+ wireless = 'wlan' in card
+ if wireless:
+ # Three independent reads, so they are worth running together.
+ with ThreadPoolExecutor(max_workers=_REFRESH_WORKERS) as pool:
+ text_job = pool.submit(_ifconfig, card)
+ status_job = pool.submit(wpa_status, card)
+ bss_job = pool.submit(wpa_cli, card, 'bss', 'current')
+ ifconfig_text = text_job.result()
+ status = status_job.result()
+ bss = _parse_key_values(bss_job.result() or '')
else:
- cmd = "ifconfig %s | grep 'inet '" % card
- out = Popen(cmd, shell=True, stdout=PIPE, universal_newlines=True)
- line = out.stdout.read().strip()
- netstate = subnetHexToDec(line)
- return netstate
+ # A wired card needs one command, and a pool for one job costs more
+ # than it saves.
+ ifconfig_text = _ifconfig(card)
+ status, bss = {}, {}
+ if wireless:
+ state = _wireless_state(ifconfig_text, status)
+ else:
+ state = _wired_state(ifconfig_text)
-def switch_default(nic):
- nics = nics_list()
- nics.remove(nic)
- if not nics:
- return
- for card in nics:
- nic_info = Popen(
- ['ifconfig', card],
- stdout=PIPE,
- close_fds=True,
- universal_newlines=True
- ).stdout.read()
- if 'status: active' in nic_info or 'status: associated' in nic_info:
- if 'inet ' in nic_info or 'inet6' in nic_info:
- run(f'service dhclient restart {card}', shell=True)
- break
- return
+ signal = None
+ if bss.get('level') and bss.get('noise'):
+ signal = bar_percent(int(bss['level']), int(bss['noise']))
+ inet = subnet_hex_to_dec(_inet_line(ifconfig_text))
+ if wireless:
+ if state['connection'] != 'Connected':
+ tooltip = f"WiFi {card} not connected"
+ else:
+ detail = f"ssid {state['ssid']} bssid {status.get('bssid', '')}"
+ tooltip = (f"Signal Strength: {signal if signal is not None else 0}% \n"
+ f"{detail} \n{inet}")
+ else:
+ tooltip = inet
+
+ return {
+ 'card': card,
+ 'kind': 'wifi' if wireless else 'wire',
+ 'connection': state['connection'],
+ 'ssid': state.get('ssid'),
+ 'signal': signal,
+ 'tooltip': tooltip,
+ }
-def restart_all_nics(widget):
- run('service netif restart', shell=True)
+def restart_all_nics(_widget):
+ """Restart every network interface.
-def stopallnetwork():
- run('service netif stop', shell=True)
+ Args:
+ _widget (Gtk.Widget): the menu item that fired this, unused.
+ """
+ _run('service', 'netif', 'restart')
-def startallnetwork():
- run('service netif start', shell=True)
+def stop_network_card(netcard):
+ """Take one interface down.
+ dhclient watches the routing socket, sees RTF_UP cleared and exits
+ through its FAIL path, which deletes the address, deletes the routes and
+ restores resolv.conf. A static card keeps its address, deliberately, so
+ that bringing it back up needs nothing but the up.
-def stopnetworkcard(netcard):
- run(f'service netif stop {netcard}', shell=True)
- switch_default(netcard)
+ Handing the default route to another interface is devd's: the link
+ going down fires src/auto-switch.py, which does exactly that.
+
+ Args:
+ netcard (str): interface name, for instance em0.
+ """
+ _run('ifconfig', netcard, 'down')
def restart_card_network(netcard):
- run(f'service netif restart {netcard}', shell=True)
+ """Restart one interface through rc.
+
+ Args:
+ netcard (str): interface name, for instance em0.
+ """
+ _run('service', 'netif', 'restart', netcard)
def restart_routing_and_dhcp(netcard):
- run('service routing restart', shell=True)
- sleep(1)
- run(f'service dhclient restart {netcard}', shell=True)
+ """Rebuild the routing table, then renew the lease on one interface.
+
+ Args:
+ netcard (str): interface name to restart dhclient on.
+ """
+ _run('service', 'routing', 'restart')
+ _run('service', 'dhclient', 'restart', netcard)
def start_static_network(netcard, inet, netmask):
- run(f'ifconfig {netcard} inet {inet} netmask {netmask}', shell=True)
- sleep(1)
- run('service routing restart', shell=True)
+ """Put a static IPv4 address on an interface and rebuild the routes.
+
+ Args:
+ netcard (str): interface name, for instance em0.
+ inet (str): IPv4 address in dotted-decimal form.
+ netmask (str): netmask in dotted-decimal form.
+ """
+ _run('ifconfig', netcard, 'inet', inet, 'netmask', netmask)
+ _run('service', 'routing', 'restart')
# IPv6 configuration functions
def start_static_ipv6_network(netcard, inet6, prefixlen):
- """Configure a static IPv6 address on the given interface."""
- run(f'ifconfig {netcard} inet6 {inet6} prefixlen {prefixlen}', shell=True)
- sleep(1)
- run('service routing restart', shell=True)
+ """Configure a static IPv6 address on an interface and rebuild the routes.
+
+ Args:
+ netcard (str): interface name, for instance em0.
+ inet6 (str): IPv6 address.
+ prefixlen (str or int): prefix length, for instance 64.
+ """
+ _run('ifconfig', netcard, 'inet6', inet6, 'prefixlen', str(prefixlen))
+ _run('service', 'routing', 'restart')
def enable_slaac(netcard):
- """Enable SLAAC (Stateless Address Autoconfiguration) on the interface
- by toggling accept_rtadv and soliciting router advertisements."""
+ """Turn on stateless address autoconfiguration for an interface.
+
+ accept_rtadv is cleared first so the interface starts from a known
+ state, then set, then router advertisements are solicited.
+
+ Args:
+ netcard (str): interface name, for instance em0.
+ """
# First disable, then re-enable to ensure clean state
- run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
+ _run('ifconfig', netcard, 'inet6', '-accept_rtadv')
sleep(0.5)
# Enable accept_rtadv for SLAAC
- run(f'ifconfig {netcard} inet6 accept_rtadv', shell=True)
+ _run('ifconfig', netcard, 'inet6', 'accept_rtadv')
# Start rtsold to solicit router advertisements
- run(f'rtsol {netcard}', shell=True)
+ _run('rtsol', netcard)
def disable_slaac(netcard):
- """Disable SLAAC on the interface."""
- run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
+ """Turn off stateless address autoconfiguration for an interface.
+
+ Args:
+ netcard (str): interface name, for instance em0.
+ """
+ _run('ifconfig', netcard, 'inet6', '-accept_rtadv')
+
+
+def start_network_card(netcard):
+ """Bring one interface up.
+
+ Nothing else is needed. The link coming up fires devd's IFNET LINK_UP
+ event, and our action for it, src/link-up.py, starts dhclient, which
+ installs the address and the routes.
+
+ Args:
+ netcard (str): interface name, for instance em0.
+ """
+ _run('ifconfig', netcard, 'up')
+
+
+def disconnect_wifi(wifi_card):
+ """Drop the current wireless association and stay disconnected.
+ `disconnect` stays disconnected until a network is selected. Downing the
+ interface does not: the daemon rejoins as soon as it comes back up.
-def get_ipv6_addresses(netcard):
- """Get all IPv6 addresses configured on the interface.
- Returns list of tuples: (address, prefixlen)."""
+ Args:
+ wifi_card (str): wireless interface name, for instance wlan0.
+
+ Returns:
+ bool: True when the daemon accepted the command.
+ """
+ return wpa_cli(wifi_card, 'disconnect') is not None
+
+
+def disable_wifi(wifi_card):
+ """Take a wireless interface down.
+
+ Args:
+ wifi_card (str): wireless interface name, for instance wlan0.
+ """
+ _run('ifconfig', wifi_card, 'down')
+
+
+def enable_wifi(wifi_card):
+ """Bring a wireless interface up.
+
+ No scan here: the refresh loop requests one on its own clock.
+
+ Args:
+ wifi_card (str): wireless interface name, for instance wlan0.
+ """
+ _run('ifconfig', wifi_card, 'up')
+
+
+def wpa_cli(wifi_card, *args):
+ """
+ Ask wpa_supplicant something through its control socket.
+
+ Args:
+ wifi_card (str): wireless interface name, which selects the socket.
+ *args (str): the wpa_cli command and its arguments, for instance
+ 'status', or 'set_network', '0', 'ssid', '"name"'.
+
+ Returns:
+ str or None: what the daemon answered, or None when it is not
+ running, has no control socket, or refused the command with FAIL.
+ """
+ # -p is where wpa_supplicant exposes its control sockets, one per
+ # interface.
+ command = ['wpa_cli', '-p', '/var/run/wpa_supplicant',
+ '-i', wifi_card] + list(args)
try:
- output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
- # Match all inet6 addresses with their prefix lengths
- addresses = re.findall(r'inet6 ([0-9a-fA-F:]+)%?\S* prefixlen (\d+)', output)
- return [(addr, int(prefixlen)) for addr, prefixlen in addresses]
- except Exception:
+ out = check_output(
+ command,
+ stderr=DEVNULL,
+ universal_newlines=True,
+ timeout=5
+ )
+ except (CalledProcessError, TimeoutExpired, OSError):
+ return None
+ if out.startswith('FAIL'):
+ return None
+ return out
+
+
+def _printf_decode(text):
+ """
+ Undo the escaping wpa_supplicant applies to an SSID on its way out.
+
+ The daemon prints every SSID through printf_encode(), so quotes and
+ backslashes come back doubled and non-ASCII bytes as \\xHH. Comparing
+ that against a plain string silently fails to match.
+
+ Args:
+ text (str): one SSID exactly as status, list_networks or
+ scan_results printed it.
+
+ Returns:
+ str: the real name. Invalid UTF-8 becomes the replacement character
+ rather than raising; a beacon can hold anything.
+ """
+ simple = {'"': 0x22, '\\': 0x5c, 'e': 0x1b, 'n': 0x0a, 'r': 0x0d,
+ 't': 0x09}
+ out = bytearray()
+ index = 0
+ while index < len(text):
+ char = text[index]
+ if char != '\\' or index + 1 >= len(text):
+ out.extend(char.encode('utf-8'))
+ index += 1
+ continue
+ marker = text[index + 1]
+ if marker in simple:
+ out.append(simple[marker])
+ index += 2
+ elif marker == 'x' and index + 3 < len(text):
+ out.append(int(text[index + 2:index + 4], 16))
+ index += 4
+ else:
+ out.extend(char.encode('utf-8'))
+ index += 1
+ return out.decode('utf-8', 'replace')
+
+
+def _parse_key_values(text):
+ """
+ Turn wpa_cli's key=value output into a dict.
+
+ Args:
+ text (str): output from a command such as status or bss.
+
+ Returns:
+ dict: every key=value line, whitespace stripped. Lines without an
+ equals sign are ignored, which is what the leading 'Selected
+ interface' banner is.
+ """
+ values = {}
+ for line in text.splitlines():
+ key, sep, value = line.partition('=')
+ if sep:
+ values[key.strip()] = value.strip()
+ return values
+
+
+def wpa_status(wifi_card):
+ """
+ Ask wpa_supplicant what it is currently doing.
+
+ Args:
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ dict: the fields of `wpa_cli status`, among them wpa_state, ssid,
+ bssid, id, key_mgmt and ip_address. Empty when the daemon cannot
+ be reached, so callers use .get() rather than testing for None.
+ """
+ out = wpa_cli(wifi_card, 'status')
+ if out is None:
+ return {}
+ values = _parse_key_values(out)
+ if 'ssid' in values:
+ values['ssid'] = _printf_decode(values['ssid'])
+ return values
+
+
+def wpa_networks(wifi_card):
+ """
+ List the saved networks the running daemon knows about.
+
+ Asking the daemon rather than parsing wpa_supplicant.conf means the two
+ cannot disagree about which networks exist or how a name was spelled.
+
+ Args:
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ list[dict]: one dict per saved network with 'id', 'ssid', 'bssid' and
+ 'flags'. Empty when the daemon cannot be reached.
+ """
+ out = wpa_cli(wifi_card, 'list_networks')
+ if out is None:
return []
+ networks = []
+ # network id / ssid / bssid / flags, one network per line after a header
+ for line in out.splitlines()[1:]:
+ fields = line.split('\t')
+ if len(fields) > 3:
+ networks.append({
+ 'id': fields[0].strip(),
+ 'ssid': _printf_decode(fields[1]),
+ 'bssid': fields[2],
+ 'flags': fields[3],
+ })
+ return networks
+
+
+def wait_for_daemon(wifi_card, timeout=5):
+ """
+ Wait for wpa_supplicant's control socket to start answering.
+ The socket may not exist yet at boot or after a hot plug. This waits;
+ it never restarts anything.
-def has_slaac_enabled(netcard):
- """Check if SLAAC (accept_rtadv) is enabled on the interface."""
- try:
- output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
- return 'ACCEPT_RTADV' in output
- except Exception:
+ Args:
+ wifi_card (str): wireless interface name.
+ timeout (int): seconds to keep trying before giving up.
+
+ Returns:
+ bool: True as soon as the daemon answers, False if it never does.
+ """
+ deadline = monotonic() + timeout
+ while True:
+ if wpa_cli(wifi_card, 'ping') is not None:
+ return True
+ if monotonic() >= deadline:
+ return False
+ sleep(0.25)
+
+
+def wpa_reconfigure(wifi_card):
+ """
+ Make wpa_supplicant re-read wpa_supplicant.conf, dropping unsaved edits.
+
+ Run before this attempt's block is written and before a forget is
+ saved, so an edit left in the daemon by an abandoned attempt is not
+ persisted with them. Network ids are handed out afresh by this, so
+ any id read before it is stale. The daemon deauthenticates on it.
+
+ Args:
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ bool: True when the daemon accepted the command.
+ """
+ return wpa_cli(wifi_card, 'reconfigure') is not None
+
+
+def wpa_network_id(wifi_card, ssid):
+ """
+ Find the daemon's id for a saved network.
+
+ Args:
+ wifi_card (str): wireless interface name.
+ ssid (str): the network name to look up.
+
+ Returns:
+ str or None: the network id, or None when the daemon has no block
+ for that name.
+ """
+ for network in wpa_networks(wifi_card):
+ if network['ssid'] == ssid:
+ return network['id']
+ return None
+
+
+def enable_all_networks(wifi_card):
+ """
+ Re-enable every saved network so the card can roam again.
+
+ select_network() disabled the others to pin one; leaving them disabled
+ would strand the card, so this must run however the attempt ended.
+
+ Args:
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ bool: True when the daemon accepted the command.
+ """
+ return wpa_cli(wifi_card, 'enable_network', 'all') is not None
+
+
+def connect_to_ssid(ssid, wifi_card):
+ """
+ Join one saved network, and only that one.
+
+ select_network disables every other saved network, so the caller must
+ call enable_all_networks() once the attempt has finished either way.
+
+ Args:
+ ssid (str): the network name to join. The daemon must already hold
+ a block for it, saved to the file or not.
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ bool: True when the daemon accepted the selection, which is the
+ start of the attempt and not its outcome. Follow it with
+ wait_for_connection().
+ """
+ if not wait_for_daemon(wifi_card):
+ return False
+ network_id = wpa_network_id(wifi_card, ssid)
+ if network_id is None:
return False
+ return wpa_cli(wifi_card, 'select_network', network_id) is not None
-def get_ipv6_gateway():
- """Get the default IPv6 gateway from routing table."""
- try:
- output = check_output('netstat -rn -f inet6', shell=True, universal_newlines=True)
- for line in output.splitlines():
- if line.startswith('default'):
- parts = line.split()
- if len(parts) >= 2:
- return parts[1]
- except Exception:
- pass
- return ""
+def _printf_encode(value):
+ """
+ Escape a value the way wpa_supplicant's own printf_encode() does.
+ The exact inverse of _printf_decode, so a name read back out of the
+ daemon can be handed straight back to it.
-def startnetworkcard(netcard):
- run(f'service netif start {netcard}', shell=True)
- sleep(1)
- run('service routing restart', shell=True)
- run(f'service dhclient start {netcard}', shell=True)
+ Args:
+ value (str): the raw text.
+ Returns:
+ str: printable ASCII, with quotes, backslashes and control
+ characters escaped and every other byte written as \\xHH.
+ """
+ simple = {0x22: '\\"', 0x5c: '\\\\', 0x1b: '\\e', 0x0a: '\\n',
+ 0x0d: '\\r', 0x09: '\\t'}
+ out = []
+ for byte in value.encode('utf-8'):
+ if byte in simple:
+ out.append(simple[byte])
+ elif 32 <= byte <= 126:
+ out.append(chr(byte))
+ else:
+ out.append(f'\\x{byte:02x}')
+ return ''.join(out)
-def wifiDisconnection(wificard):
- run(f'ifconfig {wificard} down', shell=True)
- run(f"ifconfig {wificard} ssid 'none'", shell=True)
- run(f'ifconfig {wificard} up', shell=True)
+def _quoted(value):
+ """
+ Render a value for set_network using wpa_supplicant's escaped form.
-def disableWifi(wificard):
- run(f'ifconfig {wificard} down', shell=True)
+ A plain "..." is taken literally, so hand-added backslashes are stored
+ as backslashes. P"..." is the form that gets decoded, and keeps the
+ command printable ASCII whatever the beacon held.
+ Args:
+ value (str): the raw value, such as an SSID or an identity.
-def enableWifi(wificard):
- run(f'ifconfig {wificard} up', shell=True)
- run(f'ifconfig {wificard} up scan', shell=True)
+ Returns:
+ str: the value wrapped as P"..." and ready for set_network.
+ """
+ return f'P"{_printf_encode(value)}"'
-def connectToSsid(name, wificard):
- run('killall wpa_supplicant', shell=True)
- # service
- sleep(0.5)
- run(f"ifconfig {wificard} ssid '{name}'", shell=True)
- sleep(0.5)
- wpa_supplicant = run(
- f'wpa_supplicant -B -i {wificard} -c /etc/wpa_supplicant.conf',
- shell=True
- )
- return wpa_supplicant.returncode == 0
+def _plain_quoted(value):
+ """
+ Render a value for the fields that only accept plain quotes.
+ wpa_config_parse_psk() takes a quoted passphrase or a hex key and
+ nothing else, so psk cannot use P"...". Nothing is escaped: the parser
+ reads first quote to last, so an embedded quote survives.
-def subnetHexToDec(ifconfigstring):
- snethex = re.search('0x.{8}', ifconfigstring).group(0)[2:]
- snethexlist = re.findall('..', snethex)
- snetdec = ".".join(str(int(li, 16)) for li in snethexlist)
- outputline = ifconfigstring.replace(re.search('0x.{8}', ifconfigstring).group(0), snetdec)
- return outputline
+ Args:
+ value (str): the raw passphrase.
+ Returns:
+ str: the passphrase wrapped in double quotes.
+ """
+ return f'"{value}"'
-def get_ssid_wpa_supplicant_config(ssid):
- cmd = f"""grep -A 3 'ssid="{ssid}"' /etc/wpa_supplicant.conf"""
- out = Popen(cmd, shell=True, stdout=PIPE, universal_newlines=True)
- return out.stdout.read().splitlines()
+def wpa_save_config(wifi_card):
+ """
+ Ask the daemon to write wpa_supplicant.conf, then lock the file down.
-def delete_ssid_wpa_supplicant_config(ssid):
- cmd = f"""awk '/sid="{ssid}"/ """ \
- """{print NR-2 "," NR+4 "d"}' """ \
- """/etc/wpa_supplicant.conf | sed -f - /etc/wpa_supplicant.conf"""
- out = Popen(cmd, shell=True, stdout=PIPE, universal_newlines=True)
- left_over = out.stdout.read()
- old_umask = os.umask(0o077)
- try:
- with open('/etc/wpa_supplicant.conf', 'w') as wpa_supplicant_conf:
- wpa_supplicant_conf.write(left_over)
- os.chmod('/etc/wpa_supplicant.conf', 0o600)
- finally:
- os.umask(old_umask)
+ The daemon's own chmod of the temporary file is #ifdef ANDROID, so on
+ FreeBSD the rename leaves whatever its umask produced. The file holds
+ passphrases, so 0600 has to be put back every time.
+ Args:
+ wifi_card (str): wireless interface name.
-def nic_status(card):
- out = Popen(
- f'ifconfig {card} | grep status:',
- shell=True, stdout=PIPE,
- universal_newlines=True
- )
- return out.stdout.read().split(':')[1].strip()
+ Returns:
+ bool: True when the daemon reported the file written.
+ """
+ if wpa_cli(wifi_card, 'save_config') is None:
+ return False
+ os.chmod('/etc/wpa_supplicant.conf', 0o600)
+ return True
-def start_dhcp(wificard):
- run(f'dhclient {wificard}', shell=True)
+def _save_network(wifi_card, fields):
+ """
+ Create one saved network from a list of fields, in the daemon only.
+ Nothing reaches wpa_supplicant.conf here. The caller runs
+ wpa_save_config() once the network has actually connected, so a
+ passphrase that turns out to be wrong is never written to disk.
-def wait_inet(card):
- IPREGEX = r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'
- status = 'associated' if 'wlan' in card else 'active'
- while nic_status(card) != status:
- sleep(0.1)
- print(nic_status(card))
- while True:
- ifcmd = f"ifconfig -f inet:dotted {card}"
- ifoutput = check_output(ifcmd.split(" "), universal_newlines=True)
- print(ifoutput)
- re_ip = re.search(fr'inet {IPREGEX}', ifoutput)
- if re_ip and '0.0.0.0' not in re_ip.group():
- print(re_ip)
- break
+ add_network creates the network disabled, so it is enabled here rather
+ than at save time; the config writer records the disabled flag and would
+ leave a network that never joins after a restart.
+ Args:
+ wifi_card (str): wireless interface name.
+ fields (list[tuple[str, str]]): (name, value) pairs to set, with
+ values already quoted where wpa_supplicant expects a string.
-def _escape_wpa_value(value):
- """Escape special characters for wpa_supplicant.conf quoted strings."""
- if not value:
- return value
- # Escape backslashes first, then quotes
- return value.replace('\\', '\\\\').replace('"', '\\"')
+ Returns:
+ str or None: the new network's id, or None when anything failed. A
+ half-built network is removed rather than left behind.
+ """
+ if not wait_for_daemon(wifi_card):
+ return None
+ answer = wpa_cli(wifi_card, 'add_network')
+ if answer is None:
+ return None
+ network_id = answer.strip().splitlines()[-1].strip()
+ for name, value in fields:
+ if wpa_cli(wifi_card, 'set_network', network_id, name, value) is None:
+ wpa_cli(wifi_card, 'remove_network', network_id)
+ return None
+ if wpa_cli(wifi_card, 'enable_network', network_id) is None:
+ wpa_cli(wifi_card, 'remove_network', network_id)
+ return None
+ return network_id
+
+
+def _wep_key_value(key):
+ """
+ Render a WEP key as either a hex value or a quoted ASCII one.
+
+ wpa_supplicant reads a bare wep_key0 as hex and a quoted one as ASCII,
+ and requires 5, 13 or 16 bytes either way. So only 10, 26 or 32 hex
+ digits can be meant as hex; everything else is ASCII and must be quoted.
+ Args:
+ key (str): the key exactly as the user typed it.
-def generate_eap_config(ssid, eap_config):
+ Returns:
+ str: the key ready for set_network, bare when it is hex and quoted
+ when it is ASCII.
"""
- Generate wpa_supplicant network block for EAP/Enterprise authentication.
+ if len(key) in (10, 26, 32) and all(c in hexdigits for c in key):
+ return key
+ return _quoted(key)
+
- eap_config dict should contain:
- - eap_method: 'PEAP', 'TTLS', 'TLS', etc.
- - identity: username
- - password: password (for PEAP/TTLS)
- - ca_cert: path to CA certificate (optional)
- - client_cert: path to client certificate (for TLS)
- - private_key: path to private key (for TLS)
- - private_key_passwd: private key password (for TLS)
- - phase2: inner authentication method (for PEAP/TTLS)
- - anonymous_identity: anonymous outer identity (optional)
- - domain_suffix_match: server domain validation (optional)
+def save_psk_network(ssid, security, pwd, wifi_card):
"""
- eap_method = eap_config.get('eap_method', 'PEAP')
- identity = _escape_wpa_value(eap_config.get('identity', ''))
- password = _escape_wpa_value(eap_config.get('password', ''))
- ca_cert = _escape_wpa_value(eap_config.get('ca_cert', ''))
- client_cert = _escape_wpa_value(eap_config.get('client_cert', ''))
- private_key = _escape_wpa_value(eap_config.get('private_key', ''))
- private_key_passwd = _escape_wpa_value(eap_config.get('private_key_passwd', ''))
- phase2 = eap_config.get('phase2', 'MSCHAPV2')
- anonymous_identity = _escape_wpa_value(eap_config.get('anonymous_identity', ''))
- domain_suffix_match = _escape_wpa_value(eap_config.get('domain_suffix_match', ''))
- ssid_escaped = _escape_wpa_value(ssid)
-
- ws = '\nnetwork={'
- ws += f'\n\tssid="{ssid_escaped}"'
- ws += '\n\tkey_mgmt=WPA-EAP'
- ws += f'\n\teap={eap_method}'
- ws += f'\n\tidentity="{identity}"'
+ Save a PSK or WEP network through the daemon.
- if anonymous_identity:
- ws += f'\n\tanonymous_identity="{anonymous_identity}"'
+ Args:
+ ssid (str): the network name.
+ security (str): the security type from security_from_flags, one of
+ WPA2-PSK, WPA-PSK, WPA3-SAE or WEP.
+ pwd (str): the passphrase, or the key for a WEP network.
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ str or None: the new network's id, or None when the save failed.
+
+ Raises:
+ ValueError: for a security type this writer does not cover, such as
+ enterprise or open, which have their own savers.
+ """
+ # A WPA2/WPA3 transition network reports SAE alongside PSK and joins
+ # perfectly well with a passphrase, so it is saved as RSN here.
+ if security in ('WPA2-PSK', 'WPA3-SAE'):
+ fields = [
+ ('ssid', _quoted(ssid)),
+ ('key_mgmt', 'WPA-PSK'),
+ ('proto', 'RSN'),
+ ('psk', _plain_quoted(pwd)),
+ ]
+ elif security == 'WPA-PSK':
+ fields = [
+ ('ssid', _quoted(ssid)),
+ ('key_mgmt', 'WPA-PSK'),
+ ('proto', 'WPA'),
+ ('psk', _plain_quoted(pwd)),
+ ]
+ elif security == 'WEP':
+ fields = [
+ ('ssid', _quoted(ssid)),
+ ('key_mgmt', 'NONE'),
+ ('wep_tx_keyidx', '0'),
+ ('wep_key0', _wep_key_value(pwd)),
+ ]
+ else:
+ raise ValueError(f'{security} is not a PSK or WEP network')
+ return _save_network(wifi_card, fields)
+
+
+def update_psk_network(ssid, pwd, wifi_card):
+ """
+ Change the passphrase on a network that is already saved.
+
+ Only the psk field is wrong, so it is replaced in place; deleting and
+ re-adding would discard everything else on the block.
+
+ The change is made in the daemon only. The file keeps the old passphrase
+ until the new one has connected and the caller runs wpa_save_config(),
+ so retyping a password badly cannot destroy one that worked.
+
+ Args:
+ ssid (str): the network name, which must already be saved.
+ pwd (str): the new passphrase.
+ wifi_card (str): wireless interface name.
+ Returns:
+ bool: True when the daemon took the new passphrase.
+ """
+ if not wait_for_daemon(wifi_card):
+ return False
+ network_id = wpa_network_id(wifi_card, ssid)
+ if network_id is None:
+ return False
+ if wpa_cli(wifi_card, 'set_network', network_id, 'psk',
+ _plain_quoted(pwd)) is None:
+ return False
+ return wpa_cli(wifi_card, 'enable_network', network_id) is not None
+
+
+def save_open_network(ssid, wifi_card):
+ """
+ Save an open network through the daemon.
+
+ Args:
+ ssid (str): the network name.
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ str or None: the new network's id, or None when the save failed.
+ """
+ return _save_network(wifi_card, [
+ ('ssid', _quoted(ssid)),
+ ('key_mgmt', 'NONE'),
+ ])
+
+
+def _phase2_value(eap_method, phase2):
+ """
+ Build the phase2 string for an inner authentication method.
+
+ PEAP always takes `auth=`. TTLS accepts only MSCHAPV2, MSCHAP, PAP and
+ CHAP after `auth=` (eap_ttls.c refuses to start otherwise), so EAP inner
+ methods must go through `autheap=`.
+
+ Args:
+ eap_method (str): the outer method, such as PEAP or TTLS.
+ phase2 (str): the inner method, one of PHASE2_METHODS.
+
+ Returns:
+ str: the phase2 field value, for instance "auth=MSCHAPV2" or
+ "autheap=GTC".
+ """
+ if eap_method == 'TTLS' and phase2 not in ('MSCHAPV2', 'MSCHAP', 'PAP', 'CHAP'):
+ return f'autheap={phase2}'
+ return f'auth={phase2}'
+
+
+def save_eap_network(ssid, eap_config, wifi_card):
+ """
+ Save an enterprise (802.1X/EAP) network through the daemon.
+
+ Args:
+ ssid (str): the network name.
+ eap_config (dict): the credentials collected by the EAP dialog, with
+ eap_method, identity, password, phase2, anonymous_identity,
+ ca_cert, client_cert, private_key, private_key_passwd and
+ domain_suffix_match. Only eap_method and identity are required.
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ str or None: the new network's id, or None when the save failed.
+ """
+ eap_method = eap_config.get('eap_method', 'PEAP')
+ fields = [
+ ('ssid', _quoted(ssid)),
+ ('key_mgmt', 'WPA-EAP'),
+ ('eap', eap_method),
+ ('identity', _quoted(eap_config.get('identity', ''))),
+ ]
+ anonymous_identity = eap_config.get('anonymous_identity', '')
+ if anonymous_identity:
+ fields.append(('anonymous_identity', _quoted(anonymous_identity)))
if eap_method == 'TLS':
- # TLS requires client certificate
- if client_cert:
- ws += f'\n\tclient_cert="{client_cert}"'
- if private_key:
- ws += f'\n\tprivate_key="{private_key}"'
- if private_key_passwd:
- ws += f'\n\tprivate_key_passwd="{private_key_passwd}"'
+ # TLS authenticates with a client certificate rather than a password.
+ for name in ('client_cert', 'private_key', 'private_key_passwd'):
+ value = eap_config.get(name, '')
+ if value:
+ fields.append((name, _quoted(value)))
else:
- # PEAP, TTLS, etc. use password
- ws += f'\n\tpassword="{password}"'
+ fields.append(('password', _quoted(eap_config.get('password', ''))))
+ phase2 = eap_config.get('phase2', 'MSCHAPV2')
if phase2:
- if eap_method == 'TTLS':
- ws += f'\n\tphase2="auth={phase2}"'
- else: # PEAP
- ws += f'\n\tphase2="auth={phase2}"'
+ fields.append(
+ ('phase2', _quoted(_phase2_value(eap_method, phase2)))
+ )
+ for name in ('ca_cert', 'domain_suffix_match'):
+ value = eap_config.get(name, '')
+ if value:
+ fields.append((name, _quoted(value)))
+ if not wait_for_daemon(wifi_card):
+ return None
+ # Retyping credentials replaces the block rather than adding a second
+ # one, because wpa_network_id returns the lowest id and the retry would
+ # otherwise keep selecting the credentials that just failed.
+ _remove_networks(wifi_card, ssid)
+ return _save_network(wifi_card, fields)
- if ca_cert:
- ws += f'\n\tca_cert="{ca_cert}"'
- if domain_suffix_match:
- ws += f'\n\tdomain_suffix_match="{domain_suffix_match}"'
+def _remove_networks(wifi_card, ssid):
+ """
+ Drop every saved network with this name from the daemon.
- ws += '\n}\n'
- return ws
+ Nothing is written to disk, so the caller decides whether this is a
+ removal or is making room for a replacement block.
+ Args:
+ wifi_card (str): wireless interface name.
+ ssid (str): the network name to remove.
-def write_eap_config(ssid, eap_config):
+ Returns:
+ bool: True when at least one network was removed.
"""
- Write EAP configuration to wpa_supplicant.conf with secure permissions.
+ removed = False
+ # Ids shift as networks are removed, so the list is re-read each time.
+ while True:
+ network_id = wpa_network_id(wifi_card, ssid)
+ if network_id is None:
+ break
+ if wpa_cli(wifi_card, 'remove_network', network_id) is None:
+ break
+ removed = True
+ return removed
+
+
+def forget_network(ssid, wifi_card):
"""
- config = generate_eap_config(ssid, eap_config)
- wpa_conf_path = '/etc/wpa_supplicant.conf'
+ Remove every saved network with this name and persist the removal.
- # Write with restrictive permissions (owner read/write only)
- old_umask = os.umask(0o077)
- try:
- with open(wpa_conf_path, 'a') as wsf:
- wsf.write(config)
- finally:
- os.umask(old_umask)
+ Args:
+ ssid (str): the network name to forget.
+ wifi_card (str): wireless interface name.
- # Ensure file permissions are correct
- try:
- os.chmod(wpa_conf_path, 0o600)
- except PermissionError:
- pass # May need root, handled by sudoers
+ Returns:
+ bool: True when at least one network was removed and written out.
+ """
+ if not wait_for_daemon(wifi_card):
+ return False
+ if not _remove_networks(wifi_card, ssid):
+ return False
+ return wpa_save_config(wifi_card)
+
+
+def ssid_is_saved(ssid, wifi_card):
+ """
+ Report whether the daemon already holds a network block for a name.
+
+ Asking the daemon rather than reading wpa_supplicant.conf means the two
+ cannot disagree about which networks exist or how a name was spelled.
+
+ Args:
+ ssid (str): the network name to look for.
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ bool: True when a saved network carries that name.
+ """
+ return wpa_network_id(wifi_card, ssid) is not None
+
+
+def security_from_flags(flags):
+ """
+ Read the security type out of a scan result's flags.
+
+ PSK is tested before SAE on purpose: a WPA2/WPA3 transition AP
+ advertises both as [WPA2-PSK+SAE-CCMP] and joins with a passphrase,
+ while SAE needs a wpa_supplicant built with it.
+
+ Args:
+ flags (str): the flags column, for instance '[WPA2-PSK-CCMP][ESS]'.
+
+ Returns:
+ str: one of WPA2-EAP, WPA-EAP, WPA2-PSK, WPA-PSK, WPA3-SAE, WEP or
+ OPEN.
+ """
+ modern = 'WPA2' in flags or 'RSN' in flags
+ if 'EAP' in flags:
+ return 'WPA2-EAP' if modern else 'WPA-EAP'
+ if 'PSK' in flags:
+ return 'WPA2-PSK' if modern else 'WPA-PSK'
+ if 'SAE' in flags:
+ return 'WPA3-SAE'
+ if 'WEP' in flags:
+ return 'WEP'
+ return 'OPEN'
+
+
+# Noise readings already taken, keyed by interface. See _noise_floor.
+_NOISE_FLOOR = {}
+
+
+def _noise_floor(wifi_card):
+ """
+ Return the radio's noise floor, which the scan results do not carry.
+
+ Cached per interface: noise belongs to the radio, not the access point,
+ and does not move. A failed reading is deliberately not cached, since
+ the BSS table is empty until the first scan completes.
+
+ Args:
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ int: noise in dBm from the daemon's BSS table, or -95 when the
+ driver does not report it.
+ """
+ if wifi_card in _NOISE_FLOOR:
+ return _NOISE_FLOOR[wifi_card]
+ out = wpa_cli(wifi_card, 'bss', '0')
+ if out:
+ noise = _parse_key_values(out).get('noise')
+ if noise:
+ _NOISE_FLOOR[wifi_card] = int(noise)
+ return _NOISE_FLOOR[wifi_card]
+ return -95
+
+
+def request_scan(wifi_card):
+ """
+ Ask the daemon to scan for access points now.
+
+ Unlike `ifconfig scan` this works as an unprivileged user. It is also the
+ only way to force a refresh: reading scan results never triggers one.
+
+ Args:
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ bool: True when the daemon accepted the request.
+ """
+ return wpa_cli(wifi_card, 'scan') is not None
+
+
+def request_scan_all():
+ """Ask every wireless card to scan.
+
+ Called on the refresh tick and when the menu opens, so the BSS table is
+ already warm by the time a submenu is drawn from it.
+ """
+ for card in nics_list():
+ if 'wlan' in card:
+ request_scan(card)
+
+
+def scan_networks(wifi_card):
+ """
+ List the access points in range.
+
+ Reads the BSS table only. It expires entries after bss_expiration_age
+ (180s) and autoscan refills it only while disconnected, so something
+ has to call request_scan_all() for this to stay current.
+
+ Args:
+ wifi_card (str): wireless interface name.
+
+ Returns:
+ dict: maps each SSID to a record with 'ssid', 'bssid', 'frequency',
+ 'level', 'noise', 'signal', 'flags', 'security' and 'enterprise'.
+ Strongest access point wins; hidden networks are left out.
+ """
+ out = wpa_cli(wifi_card, 'scan_results')
+ if out is None:
+ return {}
+ noise = _noise_floor(wifi_card)
+ networks = {}
+ # bssid / frequency / signal level / flags / ssid, after a header line
+ for line in out.splitlines()[1:]:
+ fields = line.split('\t')
+ if len(fields) < 5:
+ continue
+ ssid = _printf_decode(fields[4])
+ if not ssid:
+ continue
+ level = int(fields[2])
+ signal = bar_percent(level, noise)
+ if ssid in networks and networks[ssid]['signal'] >= signal:
+ continue
+ flags = fields[3]
+ security = security_from_flags(flags)
+ networks[ssid] = {
+ 'ssid': ssid,
+ 'bssid': fields[0],
+ 'frequency': int(fields[1]),
+ 'level': level,
+ 'noise': noise,
+ 'signal': signal,
+ 'flags': flags,
+ 'security': security,
+ 'enterprise': security.endswith('-EAP'),
+ }
+ return networks
+
+
+def wait_for_connection(wifi_card, ssid, timeout=30):
+ """
+ Watch a connection attempt and report whether it joined.
+
+ COMPLETED alone is not success: the daemon still reports the previous
+ association for a moment after select_network, so the reported ssid has
+ to match the one asked for.
+
+ A refused key cannot be told from a card that never answered. That would
+ need wpas_auth_failed() to fire, and on driver_bsd it does not: tested
+ 2026-09-10 with a knowingly wrong passphrase, every attempt ran the full
+ timeout without the network ever being marked TEMP-DISABLED.
+
+ Args:
+ wifi_card (str): wireless interface name.
+ ssid (str): the network the caller asked to join.
+ timeout (int): seconds to wait before giving up.
+
+ Returns:
+ bool: True once the card is joined to ssid, False on timeout.
+ """
+ deadline = monotonic() + timeout
+ while monotonic() < deadline:
+ status = wpa_status(wifi_card)
+ if status.get('wpa_state') == 'COMPLETED' and status.get('ssid') == ssid:
+ return True
+ sleep(0.5)
+ return False
+
+
+def subnet_hex_to_dec(ifconfig_string):
+ """
+ Rewrite the hexadecimal netmask in an ifconfig line as dotted decimal.
+
+ ifconfig prints `netmask 0xffffff00`, which no one reads at a glance.
+
+ Args:
+ ifconfig_string (str): an ifconfig line, normally the `inet ` one.
+
+ Returns:
+ str: the same line with the mask as 255.255.255.0. The line is
+ returned untouched when it holds no hexadecimal mask, which is
+ the case for an interface with no IPv4 address.
+ """
+ found = re.search('0x.{8}', ifconfig_string)
+ if found is None:
+ return ifconfig_string
+ snethexlist = re.findall('..', found.group(0)[2:])
+ snetdec = ".".join(str(int(li, 16)) for li in snethexlist)
+ return ifconfig_string.replace(found.group(0), snetdec)
+
+
+def wait_for_address(card, timeout=5):
+ """Wait for an interface to hold an IPv4 address.
+
+ Best effort: the caller renews the lease afterwards either way, so a
+ timeout here means a slow interface, not a failure.
+
+ Nothing waits on ifconfig's status word. An interface cannot hold a
+ leased IPv4 address with the link down, so the address is the only
+ thing worth watching, and waiting for "active" was the half that could
+ never finish on an unplugged card.
+
+ Args:
+ card (str): interface name, for instance em0 or wlan0.
+ timeout (int): seconds to wait before giving up.
+ """
+ deadline = monotonic() + timeout
+ while monotonic() < deadline:
+ ifoutput = _run('ifconfig', '-f', 'inet:dotted', card).stdout
+ if re.search(fr'inet {IP_REGEX}', ifoutput):
+ return
+ sleep(0.1)
diff --git a/NetworkMgr/query.py b/NetworkMgr/query.py
index 2f0d2dc..245c56d 100644
--- a/NetworkMgr/query.py
+++ b/NetworkMgr/query.py
@@ -1,29 +1,90 @@
#!/usr/bin/env python
-from subprocess import check_output
+"""Read-only queries for the current network configuration.
+
+Answers what the configuration UI needs to know about an interface: its
+assignment method, addresses, gateway, DNS servers and search domain, for
+both IPv4 and IPv6. Values come from rc.conf(5) via sysrc, from ifconfig,
+from the routing table and from /etc/resolv.conf.
+"""
+
+from subprocess import CalledProcessError, check_output, run
import re
import os
+IP_REGEX = r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'
+
+
+def _rc_conf_value(name):
+ """Return the effective value of an rc.conf(5) variable.
+
+ sysrc reads every file in rc_conf_files, so a setting placed in
+ /etc/rc.conf.local is seen here and overrides /etc/rc.conf, matching
+ both the boot-time behaviour and where our own sysrc writes land.
+
+ Args:
+ name (str): The rc.conf variable to read, such as "defaultrouter".
+
+ Returns:
+ str: The variable's value with surrounding whitespace removed, or an
+ empty string if it is set nowhere.
+ """
+ sysrc = run(
+ ['sysrc', '-n', name],
+ capture_output=True,
+ universal_newlines=True,
+ check=False
+ )
+ if sysrc.returncode != 0:
+ return ""
+ return sysrc.stdout.strip()
+
+
+def _address_after(keyword, text):
+ """Read the dotted-quad address that follows a keyword.
+
+ Args:
+ keyword (str): the word before the address, such as "netmask".
+ text (str): ifconfig output.
+
+ Returns:
+ str: the address, or an empty string when the keyword is not there.
+ A missing field is ordinary rather than exceptional: a loopback or
+ point-to-point interface carries an address with no broadcast.
+ """
+ found = re.search(fr'{keyword} {IP_REGEX}', text)
+ if not found:
+ return ""
+ return found.group().replace(f'{keyword} ', '').strip()
+
def get_interface_settings_ipv6(active_nic):
- """Get IPv6 settings for the given network interface."""
+ """Collect the IPv6 settings of one network interface.
+
+ Args:
+ active_nic (str): Interface name, such as "em0" or "wlan0".
+
+ Returns:
+ dict[str, str]: Assignment Method, Interface IPv6, Prefix Length,
+ Default Gateway, DNS Server 1 and Search Domain. Missing values are
+ empty strings rather than absent keys.
+ """
ipv6_settings = {}
- rc_conf = open("/etc/rc.conf", "r").read()
+ ifconfig_ipv6 = _rc_conf_value(f'ifconfig_{active_nic}_ipv6')
# Check if SLAAC is enabled (accept_rtadv in rc.conf)
slaac_search = re.search(
- fr'^ifconfig_{active_nic}_ipv6=".*accept_rtadv',
- rc_conf,
- re.MULTILINE | re.IGNORECASE
+ r'accept_rtadv',
+ ifconfig_ipv6,
+ re.IGNORECASE
)
if slaac_search:
ipv6_settings["Assignment Method"] = "SLAAC"
else:
# Check for static IPv6 configuration
static_search = re.search(
- fr'^ifconfig_{active_nic}_ipv6="inet6\s+([0-9a-fA-F:]+).*prefixlen\s+(\d+)',
- rc_conf,
- re.MULTILINE
+ r'^inet6\s+([0-9a-fA-F:]+).*prefixlen\s+(\d+)',
+ ifconfig_ipv6
)
if static_search:
ipv6_settings["Assignment Method"] = "Manual"
@@ -50,19 +111,18 @@ def get_interface_settings_ipv6(active_nic):
else:
ipv6_settings["Interface IPv6"] = ""
ipv6_settings["Prefix Length"] = "64"
- except Exception:
+ except (CalledProcessError, OSError):
ipv6_settings["Interface IPv6"] = ""
ipv6_settings["Prefix Length"] = "64"
- # Get IPv6 default gateway from rc.conf or routing table
- # Pattern allows optional interface suffix for link-local (e.g., fe80::1%em0)
- gateway_search = re.search(
- r'^ipv6_defaultrouter="([0-9a-fA-F:]+(?:%[a-zA-Z0-9]+)?)"',
- rc_conf,
- re.MULTILINE
+ # The suffix allows a link-local gateway (fe80::1%em0). An unset
+ # variable reads as the "NO" sentinel, which fails this match.
+ gateway_search = re.fullmatch(
+ r'[0-9a-fA-F:]+(?:%[a-zA-Z0-9]+)?',
+ _rc_conf_value('ipv6_defaultrouter')
)
if gateway_search:
- ipv6_settings["Default Gateway"] = gateway_search.group(1)
+ ipv6_settings["Default Gateway"] = gateway_search.group()
else:
# Try to get from routing table
try:
@@ -80,13 +140,14 @@ def get_interface_settings_ipv6(active_nic):
break
else:
ipv6_settings["Default Gateway"] = ""
- except Exception:
+ except (CalledProcessError, OSError):
ipv6_settings["Default Gateway"] = ""
# Get IPv6 DNS servers from resolv.conf
ipv6_settings["DNS Server 1"] = ""
if os.path.exists('/etc/resolv.conf'):
- resolv_conf = open('/etc/resolv.conf').read()
+ with open('/etc/resolv.conf', encoding='utf-8') as resolv_file:
+ resolv_conf = resolv_file.read()
# Match IPv6 nameservers (must contain at least one colon)
ipv6_nameservers = re.findall(
r'^nameserver\s+([0-9a-fA-F]*:[0-9a-fA-F:]+)',
@@ -99,7 +160,8 @@ def get_interface_settings_ipv6(active_nic):
# Get search domain (shared with IPv4)
ipv6_settings["Search Domain"] = ""
if os.path.exists('/etc/resolv.conf'):
- resolv_conf = open('/etc/resolv.conf').read()
+ with open('/etc/resolv.conf', encoding='utf-8') as resolv_file:
+ resolv_conf = resolv_file.read()
search_match = re.search(r'^search\s+(.+)$', resolv_conf, re.MULTILINE)
if search_match:
ipv6_settings["Search Domain"] = search_match.group(1).strip()
@@ -112,79 +174,79 @@ def get_interface_settings_ipv6(active_nic):
def get_interface_settings(active_nic):
+ """Collect the IPv4 settings of one network interface.
+
+ Args:
+ active_nic (str): Interface name, such as "em0" or "wlan0".
+
+ Returns:
+ dict[str, str]: Active Interface, Assignment Method, Interface IP,
+ Interface Subnet Mask, Broadcast Address, Default Gateway, Search
+ Domain and one "DNS Server N" entry per nameserver. DNS Server 1 and
+ 2 are always present, empty when unset.
+ """
interface_settings = {}
- rc_conf = open("/etc/rc.conf", "r").read()
- DHCPSearch = re.findall(fr'^ifconfig_{active_nic}=".*DHCP', rc_conf, re.MULTILINE)
- print(f"DHCPSearch is {DHCPSearch} and the length is {len(DHCPSearch)}")
- if len(DHCPSearch) < 1:
- DHCPStatusOutput = "Manual"
+ if 'DHCP' in _rc_conf_value(f'ifconfig_{active_nic}'):
+ dhcp_status_output = "DHCP"
else:
- DHCPStatusOutput = "DHCP"
-
- IPREGEX = r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'
+ dhcp_status_output = "Manual"
ifcmd = f"ifconfig -f inet:dotted {active_nic}"
ifoutput = check_output(ifcmd.split(" "), universal_newlines=True)
- re_ip = re.search(fr'inet {IPREGEX}', ifoutput)
- if re_ip:
- if_ip = re_ip.group().replace("inet ", "").strip()
- re_netmask = re.search(fr'netmask {IPREGEX}', ifoutput)
- if_netmask = re_netmask.group().replace("netmask ", "").strip()
- re_broadcast = re.search(fr'broadcast {IPREGEX}', ifoutput)
- if_broadcast = re_broadcast.group().replace("broadcast ", "").strip()
- else:
- if_ip = ""
- if_netmask = ""
- if_broadcast = ""
- if (DHCPStatusOutput == "DHCP"):
+ if_ip = _address_after('inet', ifoutput)
+ if_netmask = _address_after('netmask', ifoutput)
+ if_broadcast = _address_after('broadcast', ifoutput)
+ if dhcp_status_output == "DHCP":
dhclient_leases = f"/var/db/dhclient.leases.{active_nic}"
if os.path.exists(dhclient_leases) is False:
- print("DHCP is enabled, but we're unable to read the lease "
- f"file a /var/db/dhclient.leases.{active_nic}")
+ # No lease file yet, so there is no router option to read. The
+ # window shows an empty gateway rather than a stale one.
gateway = ""
else:
- dh_lease = open(dhclient_leases, "r").read()
- re_gateway = re.search(fr"option routers {IPREGEX}", dh_lease)
- gateway = re_gateway.group().replace("option routers ", "")
+ with open(dhclient_leases, "r", encoding='utf-8') as lease_file:
+ dh_lease = lease_file.read()
+ # dhclient appends leases, so the last one is the current one.
+ routers = re.findall(fr'option routers ({IP_REGEX})', dh_lease)
+ gateway = routers[-1] if routers else ""
else:
- rc_conf = open('/etc/rc.conf', 'r').read()
- re_gateway = re.search(fr'^defaultrouter="{IPREGEX}"', rc_conf, re.MULTILINE)
+ # An unset defaultrouter reads as the "NO" sentinel from
+ # /etc/defaults/rc.conf, which fails this match.
+ re_gateway = re.fullmatch(IP_REGEX, _rc_conf_value('defaultrouter'))
if re_gateway:
- gateway = re_gateway.group().replace('"', "")
- gateway = gateway.replace('defaultrouter=', "")
+ gateway = re_gateway.group()
else:
gateway = ""
if os.path.exists('/etc/resolv.conf'):
- resolv_conf = open('/etc/resolv.conf').read()
- nameservers = re.findall(fr'^nameserver {IPREGEX}', str(resolv_conf), re.MULTILINE)
- print(nameservers)
-
- re_domain_search = re.findall('search [a-zA-Z.]*', str(resolv_conf))
- if len(re_domain_search) < 1:
- re_domain_search = re.findall('domain (.*)', resolv_conf)
- domain_search = str(re_domain_search).replace("domain ", "")
- domain_search = domain_search.replace("'", "")
- domain_search = domain_search.replace("[", "")
- domain_search = domain_search.replace("]", "")
- domain_search = domain_search.replace('search', '').strip()
+ with open('/etc/resolv.conf', encoding='utf-8') as resolv_file:
+ resolv_conf = resolv_file.read()
+ nameservers = re.findall(fr'^nameserver {IP_REGEX}', str(resolv_conf), re.MULTILINE)
+
+ domain_search = ''
+ search_match = re.search(r'^search\s+(.+)$', resolv_conf, re.MULTILINE)
+ if search_match:
+ domain_search = search_match.group(1).strip()
+ else:
+ domain_match = re.search(r'^domain\s+(.+)$', resolv_conf, re.MULTILINE)
+ if domain_match:
+ domain_search = domain_match.group(1).strip()
else:
domain_search = ''
nameservers = []
interface_settings["Active Interface"] = active_nic
- interface_settings["Assignment Method"] = DHCPStatusOutput
+ interface_settings["Assignment Method"] = dhcp_status_output
interface_settings["Interface IP"] = if_ip
interface_settings["Interface Subnet Mask"] = if_netmask
interface_settings["Broadcast Address"] = if_broadcast
interface_settings["Default Gateway"] = gateway
interface_settings["Search Domain"] = domain_search
- for num in range(len(nameservers)):
+ for num, nameserver in enumerate(nameservers):
interface_settings[
f"DNS Server {num + 1}"
- ] = str(nameservers[(num)]).replace("nameserver", "").strip()
+ ] = str(nameserver).replace("nameserver", "").strip()
# if DNS Server 1 and 2 are missing create them with empty string
if "DNS Server 1" not in interface_settings:
interface_settings["DNS Server 1"] = ""
diff --git a/NetworkMgr/trayicon.py b/NetworkMgr/trayicon.py
index fea26fe..e2224c2 100755
--- a/NetworkMgr/trayicon.py
+++ b/NetworkMgr/trayicon.py
@@ -1,90 +1,180 @@
#!/usr/bin/env python
-import gi
-gi.require_version('Gtk', '3.0')
+"""The NetworkMgr tray icon and its menu.
+
+Owns everything the user sees from the system tray: the interface and
+network menus, the passphrase and enterprise credential dialogs, the signal
+icons, and the background thread that refreshes all of it. Every system
+call it makes goes through NetworkMgr.net_api or NetworkMgr.wg_api.
+"""
+
import gettext
import threading
import _thread
-import locale
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk, GObject, GLib, Pango
from time import sleep
-from gi.repository import Gtk, GObject, GLib
from NetworkMgr.net_api import (
- stopnetworkcard,
- startnetworkcard,
- wifiDisconnection,
+ stop_network_card,
+ start_network_card,
+ disconnect_wifi,
restart_all_nics,
- stopallnetwork,
- startallnetwork,
- connectToSsid,
- disableWifi,
- enableWifi,
- connectionStatus,
- networkdictionary,
- delete_ssid_wpa_supplicant_config,
- nic_status,
+ connect_to_ssid,
+ enable_all_networks,
+ disable_wifi,
+ enable_wifi,
+ network_dictionary,
+ request_scan_all,
+ scan_networks,
+ tray_state,
+ forget_network,
+ ssid_is_saved,
+ save_psk_network,
+ update_psk_network,
+ save_open_network,
+ wpa_networks,
+ wpa_reconfigure,
+ wpa_save_config,
+ wait_for_connection,
EAP_METHODS,
PHASE2_METHODS,
- DEFAULT_CA_CERT,
- is_enterprise_network,
- get_security_type,
- validate_certificate,
get_system_ca_certificates,
- write_eap_config
+ save_eap_network
)
-from NetworkMgr.configuration import network_card_configuration
+from NetworkMgr.configuration import open_configuration
from NetworkMgr.wg_api import (
wg_dictionary,
+ wg_service_state,
disable_wg,
- enable_wg,
- wg_status
+ enable_wg
)
gettext.bindtextdomain('networkmgr', '/usr/local/share/locale')
gettext.textdomain('networkmgr')
_ = gettext.gettext
-encoding = locale.getpreferredencoding()
-threadBreak = False
GObject.threads_init()
+# Attempts on one network before its saved block is removed. One failure is
+# not proof of a wrong password (issue #81); three is enough either way.
+MAX_ATTEMPTS = 3
+
+
+
+def _heading_label(text):
+ """Build a dialog heading without going through the markup parser.
+
+ Headings carry an SSID, so a name holding `&` or a tag would break the
+ Pango parse or have its markup obeyed. Attributes leave the text alone.
+
+ Args:
+ text (str): the heading, already translated.
+
+ Returns:
+ Gtk.Label: the label, bold and one size up, showing text verbatim.
+ """
+ label = Gtk.Label(label=text)
+ attributes = Pango.AttrList()
+ attributes.insert(Pango.attr_weight_new(Pango.Weight.BOLD))
+ # PANGO_SCALE_LARGE is a C macro and not introspectable.
+ attributes.insert(Pango.attr_scale_new(1.2))
+ label.set_attributes(attributes)
+ return label
-class trayIcon(object):
- def stop_manager(self, widget):
+class TrayIcon:
+ """The status icon, its menu, and the thread that keeps them current."""
+
+ def stop_manager(self, _widget):
+ """Quit the application.
+
+ Args:
+ _widget (Gtk.Widget): the menu item that fired this, unused.
+ """
Gtk.main_quit()
def __init__(self):
- self.if_running = False
self.cardinfo = None
- self.statusIcon = Gtk.StatusIcon()
- self.statusIcon.set_visible(True)
- self.statusIcon.connect("activate", self.leftclick)
- self.statusIcon.connect('popup-menu', self.icon_clicked)
-
- def leftclick(self, status_icon):
- if not self.thr.is_alive():
- self.thr.start()
+ self.status_icon = Gtk.StatusIcon()
+ self.status_icon.set_visible(True)
+ self.status_icon.connect("activate", self.left_click)
+ self.status_icon.connect('popup-menu', self.menu_requested)
+ # Built on demand by the menu and dialog builders below.
+ self.thr = None
+ self.menu = None
+ # Set while an attempt runs so the refresh does not draw over the
+ # animation. Written from the worker; bool assignment is atomic.
+ self.connecting = False
+ self.connect_frame = 0
+ # Failed attempts since the user last picked this network from the
+ # menu. At three the saved block is removed, see MAX_ATTEMPTS.
+ self.connect_attempts = 0
+ self.traystate = tray_state()
+ # Read once: 13 ms of shell, and it only changes on an rc.conf edit.
+ self.wg_service = wg_service_state()
+ self.window = None
+ self.password = None
+ self.eap_window = None
+ self.eap_method_combo = None
+ self.phase2_combo = None
+ self.identity_entry = None
+ self.anon_identity_entry = None
+ self.eap_password = None
+ self.ca_cert_chooser = None
+ self.client_cert_chooser = None
+ self.private_key_chooser = None
+ self.private_key_passwd = None
+ self.eap_rows = {}
+
+ def left_click(self, status_icon):
+ """Pop the menu up under a left click on the tray icon.
+
+ Args:
+ status_icon (Gtk.StatusIcon): the icon that was clicked.
+ """
button = 1
time = Gtk.get_current_event_time()
position = Gtk.StatusIcon.position_menu
- self.nm_menu().popup(None, None, position, status_icon, button, time)
+ self.build_menu().popup(None, None, position, status_icon, button, time)
+
+ def menu_requested(self, status_icon, button, time):
+ """Pop the menu up for a right click or a popup-menu key press.
- def icon_clicked(self, status_icon, button, time):
- if not self.thr.is_alive():
- self.thr.start()
+ Args:
+ status_icon (Gtk.StatusIcon): the icon that was clicked.
+ button (int): the mouse button number GTK reported.
+ time (int): the event's timestamp, passed straight to popup().
+ """
position = Gtk.StatusIcon.position_menu
- self.nm_menu().popup(None, None, position, status_icon, button, time)
+ self.build_menu().popup(None, None, position, status_icon, button, time)
- def nm_menu(self):
+ def build_menu(self):
+ """Build the whole tray menu.
+
+ The refresh tick collects only what the icon and the tooltip show,
+ so the interfaces are read here instead. That is about 10 ms, which
+ a click can afford. The scan list under each wireless card costs
+ more again, so it is filled when its submenu opens rather than now.
+
+ Returns:
+ Gtk.Menu: the assembled menu, ready to pop up.
+ """
self.menu = Gtk.Menu()
- if len(self.wginfo['configs'])> 0 and self.wginfo['service'] == '"NO"':
+ # Asked for, not waited on: this refreshes the list for the next
+ # look, while the submenu below is drawn from what is known now.
+ request_scan_all()
+ # The refresh tick deliberately does not collect this.
+ self.cardinfo = network_dictionary()
+ wg_info = wg_dictionary(self.wg_service)
+ if len(wg_info['configs']) > 0 and wg_info['service'] == 'NO':
wg_title = Gtk.MenuItem()
wg_title.set_label(_("WireGuard VPN"))
wg_title.set_sensitive(False)
self.menu.append(wg_title)
self.menu.append(Gtk.SeparatorMenuItem())
- wg_devices = self.wginfo['configs']
+ wg_devices = wg_info['configs']
for wg_dev in wg_devices:
connection_state = wg_devices[wg_dev]['state']
connection_info = wg_devices[wg_dev]['info']
@@ -92,15 +182,15 @@ def nm_menu(self):
wg_item = Gtk.MenuItem(_("%s Connected") % connection_info)
wg_item.set_sensitive(False)
self.menu.append(wg_item)
- disconnectwg_item = Gtk.ImageMenuItem(_(f"Disable {wg_dev}"))
- disconnectwg_item.connect("activate", self.disconnectWG, wg_dev)
+ disconnectwg_item = Gtk.ImageMenuItem(_("Disable %s") % wg_dev)
+ disconnectwg_item.connect("activate", self.disconnect_wg, wg_dev)
self.menu.append(disconnectwg_item)
else:
notonlinewg = Gtk.MenuItem(_("%s Disconnected") % connection_info)
notonlinewg.set_sensitive(False)
self.menu.append(notonlinewg)
wiredwg_item = Gtk.MenuItem(_("Enable"))
- wiredwg_item.connect("activate", self.connectWG, wg_dev)
+ wiredwg_item.connect("activate", self.connect_wg, wg_dev)
self.menu.append(wiredwg_item)
self.menu.append(Gtk.SeparatorMenuItem())
@@ -115,24 +205,35 @@ def nm_menu(self):
for netcard in cards:
connection_state = cards[netcard]['state']["connection"]
if "wlan" not in netcard:
- if connection_state == "Connected":
+ if connection_state == "Disabled":
+ wd_title = Gtk.MenuItem(_("Wired %s Disabled") % cardnum)
+ wd_title.set_sensitive(False)
+ self.menu.append(wd_title)
+ wired_item = Gtk.MenuItem(_("Enable"))
+ wired_item.connect("activate", self.enable_card, netcard)
+ self.menu.append(wired_item)
+ elif connection_state == "Connected":
wired_item = Gtk.MenuItem(_("Wired %s Connected") % cardnum)
wired_item.set_sensitive(False)
self.menu.append(wired_item)
- disconnect_item = Gtk.ImageMenuItem(_(f"Disable {netcard}"))
- disconnect_item.connect("activate", self.disconnectcard,
+ disconnect_item = Gtk.ImageMenuItem(_("Disable %s") % netcard)
+ disconnect_item.connect("activate", self.disable_card,
netcard)
self.menu.append(disconnect_item)
- configure_item = Gtk.ImageMenuItem(f"Configure {netcard}")
+ configure_item = Gtk.ImageMenuItem(_("Configure %s") % netcard)
configure_item.connect("activate", self.configuration_window_open, netcard)
self.menu.append(configure_item)
elif connection_state == "Disconnected":
notonline = Gtk.MenuItem(_("Wired %s Disconnected") % cardnum)
notonline.set_sensitive(False)
self.menu.append(notonline)
- wired_item = Gtk.MenuItem(_("Enable"))
- wired_item.connect("activate", self.connectcard, netcard)
- self.menu.append(wired_item)
+ disconnect_item = Gtk.ImageMenuItem(_("Disable %s") % netcard)
+ disconnect_item.connect("activate", self.disable_card,
+ netcard)
+ self.menu.append(disconnect_item)
+ configure_item = Gtk.ImageMenuItem(_("Configure %s") % netcard)
+ configure_item.connect("activate", self.configuration_window_open, netcard)
+ self.menu.append(configure_item)
else:
disconnected = Gtk.MenuItem(_("Wired %s Unplug") % cardnum)
disconnected.set_sensitive(False)
@@ -146,36 +247,38 @@ def nm_menu(self):
wd_title.set_sensitive(False)
self.menu.append(wd_title)
enawifi = Gtk.MenuItem(_("Enable Wifi %s") % wifinum)
- enawifi.connect("activate", self.enable_Wifi, netcard)
+ enawifi.connect("activate", self.enable_wifi, netcard)
self.menu.append(enawifi)
elif connection_state == "Disconnected":
d_title = Gtk.MenuItem()
d_title.set_label(_("WiFi %s Disconnected") % wifinum)
d_title.set_sensitive(False)
self.menu.append(d_title)
- self.wifiListMenu(netcard, None, False, cards)
+ self.wifi_list_menu(netcard, None, False)
+ self.forget_list_menu(netcard)
diswifi = Gtk.MenuItem(_("Disable Wifi %s") % wifinum)
- diswifi.connect("activate", self.disable_Wifi, netcard)
+ diswifi.connect("activate", self.disable_wifi, netcard)
self.menu.append(diswifi)
else:
ssid = cards[netcard]['state']["ssid"]
- bar = cards[netcard]['info'][ssid][4]
+ signal = cards[netcard]['signal'] or 0
wc_title = Gtk.MenuItem(_("WiFi %s Connected") % wifinum)
wc_title.set_sensitive(False)
self.menu.append(wc_title)
connection_item = Gtk.ImageMenuItem(ssid)
- connection_item.set_image(self.wifi_signal_icon(bar))
+ connection_item.set_image(self.wifi_signal_icon(signal))
connection_item.show()
disconnect_item = Gtk.MenuItem(_("Disconnect from %s") % ssid)
disconnect_item.connect("activate", self.disconnect_wifi,
netcard)
self.menu.append(connection_item)
self.menu.append(disconnect_item)
- self.wifiListMenu(netcard, ssid, True, cards)
+ self.wifi_list_menu(netcard, ssid, True)
+ self.forget_list_menu(netcard)
diswifi = Gtk.MenuItem(_("Disable Wifi %s") % wifinum)
- diswifi.connect("activate", self.disable_Wifi, netcard)
+ diswifi.connect("activate", self.disable_wifi, netcard)
self.menu.append(diswifi)
- configure_item = Gtk.ImageMenuItem(f"Configure {netcard}")
+ configure_item = Gtk.ImageMenuItem(_("Configure %s") % netcard)
configure_item.connect("activate", self.configuration_window_open, netcard)
self.menu.append(configure_item)
self.menu.append(Gtk.SeparatorMenuItem())
@@ -190,234 +293,582 @@ def nm_menu(self):
self.menu.show_all()
return self.menu
- def ssid_menu_item(self, sn, caps, ssid, ssid_info, wificard):
+ def ssid_menu_item(self, network, wifi_card):
+ """
+ Build one clickable access point entry for the menu.
+
+ Args:
+ network (dict): the network's scan record from scan_networks.
+ wifi_card (str): the interface the entry would connect.
+
+ Returns:
+ Gtk.ImageMenuItem: labelled with the SSID, carrying a signal icon
+ that shows a padlock for anything but an open network, and
+ wired to the dialog its security type calls for. A WPA3-only
+ network is shown greyed out: driver_bsd offers no SAE.
+ """
+ ssid = network['ssid']
+ if network['security'] == 'WPA3-SAE':
+ menu_item = Gtk.ImageMenuItem(_("%s (WPA3 only)") % ssid)
+ menu_item.set_image(self.wifi_signal_icon(network['signal'], True))
+ menu_item.set_sensitive(False)
+ menu_item.show()
+ return menu_item
menu_item = Gtk.ImageMenuItem(ssid)
- # Check if enterprise network (index 9 contains enterprise flag boolean)
- is_enterprise = len(ssid_info) > 9 and ssid_info[9] is True
- if caps in ('E', 'ES'):
+ if network['security'] == 'OPEN':
is_secure = False
- click_action = self.menu_click_open
- ssid_type = ssid
- elif is_enterprise:
+ click_action = self.connect_open_network
+ click_argument = ssid
+ elif network['enterprise']:
is_secure = True
- click_action = self.menu_click_enterprise
- ssid_type = ssid_info
+ click_action = self.connect_enterprise_network
+ click_argument = network
else:
is_secure = True
- click_action = self.menu_click_lock
- ssid_type = ssid_info
- menu_item.set_image(self.wifi_signal_icon(sn, is_secure))
- menu_item.connect("activate", click_action, ssid_type, wificard)
+ click_action = self.connect_psk_network
+ click_argument = network
+ menu_item.set_image(self.wifi_signal_icon(network['signal'], is_secure))
+ menu_item.connect("activate", click_action, click_argument, wifi_card)
menu_item.show()
return menu_item
- def wifiListMenu(self, wificard, cssid, passes, cards):
+ def wifi_list_menu(self, wifi_card, cssid, passes):
+ """
+ Add the "Available Connections" submenu for one wireless card.
+
+ The submenu fills itself when opened, so a tray click does not pay
+ for a scan read.
+
+ Args:
+ wifi_card (str): the interface whose scan results to list.
+ cssid (str or None): the SSID already connected, if any.
+ passes (bool): True to leave cssid out of the list, so the card
+ the user is already on is not offered again.
+ """
wiconncmenu = Gtk.Menu()
avconnmenu = Gtk.MenuItem(_("Available Connections"))
avconnmenu.set_submenu(wiconncmenu)
- for ssid in cards[wificard]['info']:
- ssid_info = cards[wificard]['info'][ssid]
- ssid = cards[wificard]['info'][ssid][0]
- sn = cards[wificard]['info'][ssid][4]
- caps = cards[wificard]['info'][ssid][6]
- if passes:
- if cssid != ssid:
- menu_item = self.ssid_menu_item(sn, caps, ssid, ssid_info, wificard)
- wiconncmenu.append(menu_item)
- else:
- menu_item = self.ssid_menu_item(sn, caps, ssid, ssid_info, wificard)
- wiconncmenu.append(menu_item)
+ wiconncmenu.connect("show", self.fill_wifi_list, wifi_card, cssid,
+ passes)
self.menu.append(avconnmenu)
- def configuration_window_open(self, widget, interface):
- network_card_configuration(interface)
-
- def menu_click_open(self, widget, ssid, wificard):
- if f'"{ssid}"' in open("/etc/wpa_supplicant.conf").read():
- connectToSsid(ssid, wificard)
- else:
- self.Open_Wpa_Supplicant(ssid, wificard)
- self.updateinfo()
-
- def menu_click_lock(self, widget, ssid_info, wificard):
- if f'"{ssid_info[0]}"' in open('/etc/wpa_supplicant.conf').read():
- connectToSsid(ssid_info[0], wificard)
+ def forget_list_menu(self, wifi_card):
+ """
+ Add the "Forget Network" submenu for one wireless card.
+
+ The only way to correct a network saved with the wrong passphrase:
+ clicking it connects with the stored key, and an access point that
+ never answers never refuses, so nothing reopens the dialog.
+
+ Args:
+ wifi_card (str): the interface whose saved networks to list.
+ """
+ forget_menu = Gtk.Menu()
+ forget_item = Gtk.MenuItem(_("Forget Network"))
+ forget_item.set_submenu(forget_menu)
+ forget_menu.connect("show", self.fill_forget_list, wifi_card)
+ self.menu.append(forget_item)
+
+ def fill_forget_list(self, submenu, wifi_card):
+ """
+ List the saved networks, filled when the submenu is opened.
+
+ These come from the daemon rather than a scan, so a network that is
+ saved but out of range can still be forgotten.
+
+ Args:
+ submenu (Gtk.Menu): the submenu to fill.
+ wifi_card (str): the interface whose saved networks to list.
+ """
+ for child in submenu.get_children():
+ submenu.remove(child)
+ for network in wpa_networks(wifi_card):
+ entry = Gtk.MenuItem(network['ssid'])
+ entry.connect("activate", self.forget_ssid, network['ssid'],
+ wifi_card)
+ entry.show()
+ submenu.append(entry)
+
+ def forget_ssid(self, _widget, ssid, wifi_card):
+ """
+ Remove one saved network.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ ssid (str): the network to forget.
+ wifi_card (str): the interface it is saved on.
+ """
+ wpa_reconfigure(wifi_card)
+ forget_network(ssid, wifi_card)
+ self.update_info()
+
+ def fill_wifi_list(self, submenu, wifi_card, cssid, passes):
+ """
+ Read the scan table and fill the Available Connections submenu.
+
+ Args:
+ submenu (Gtk.Menu): the submenu to fill.
+ wifi_card (str): the interface whose scan results to list.
+ cssid (str or None): the SSID already connected, if any.
+ passes (bool): True to leave cssid out of the list.
+ """
+ for child in submenu.get_children():
+ submenu.remove(child)
+ for ssid, network in scan_networks(wifi_card).items():
+ if passes and cssid == ssid:
+ continue
+ submenu.append(self.ssid_menu_item(network, wifi_card))
+
+ def configuration_window_open(self, _widget, interface):
+ """Open the configuration window for one interface.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ interface (str): interface name to configure.
+ """
+ open_configuration(interface)
+
+ def connect_open_network(self, _widget, ssid, wifi_card):
+ """
+ Join an open network, asking for nothing.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ ssid (str): the network name that was clicked.
+ wifi_card (str): the interface to connect.
+ """
+ self.connect_attempts = 0
+ wpa_reconfigure(wifi_card)
+ if not ssid_is_saved(ssid, wifi_card) \
+ and save_open_network(ssid, wifi_card) is None:
+ self.connection_failed(ssid, wifi_card)
+ return
+ self.start_connection(ssid, None, wifi_card)
+
+ def connect_psk_network(self, _widget, network, wifi_card):
+ """
+ Join a passphrase network, asking for the password if it is new.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ network (dict): the clicked network's scan record.
+ wifi_card (str): the interface to connect.
+ """
+ self.connect_attempts = 0
+ if ssid_is_saved(network['ssid'], wifi_card):
+ wpa_reconfigure(wifi_card)
+ self.start_connection(network['ssid'], network, wifi_card)
else:
- self.Authentication(ssid_info, wificard, False)
- self.updateinfo()
-
- def menu_click_enterprise(self, widget, ssid_info, wificard):
- ssid_configured = False
- try:
- with open('/etc/wpa_supplicant.conf', 'r') as conf:
- ssid_configured = f'"{ssid_info[0]}"' in conf.read()
- except (FileNotFoundError, PermissionError, IOError):
- ssid_configured = False
- if ssid_configured:
- connectToSsid(ssid_info[0], wificard)
+ self.passphrase_dialog(network, wifi_card, False)
+
+ def connect_enterprise_network(self, _widget, network, wifi_card):
+ """
+ Join an enterprise network, asking for credentials if it is new.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ network (dict): the clicked network's scan record.
+ wifi_card (str): the interface to connect.
+ """
+ self.connect_attempts = 0
+ if ssid_is_saved(network['ssid'], wifi_card):
+ wpa_reconfigure(wifi_card)
+ self.start_connection(network['ssid'], network, wifi_card,
+ enterprise=True)
else:
- self.EnterpriseAuthentication(ssid_info, wificard, False)
- self.updateinfo()
-
- def disconnect_wifi(self, widget, wificard):
- wifiDisconnection(wificard)
- self.updateinfo()
-
- def disable_Wifi(self, widget, wificard):
- disableWifi(wificard)
- self.updateinfo()
-
- def enable_Wifi(self, widget, wificard):
- enableWifi(wificard)
- self.updateinfo()
-
- def connectcard(self, widget, netcard):
- startnetworkcard(netcard)
- self.updateinfo()
-
- def disconnectcard(self, widget, netcard):
- stopnetworkcard(netcard)
- self.updateinfo()
-
- def connectWG(self, widget, wginfo):
- enable_wg(wginfo)
- self.updateinfo()
-
- def disconnectWG(self, widget, wginfo):
- disable_wg(wginfo)
- self.updateinfo()
-
- def closeNetwork(self, widget):
- stopallnetwork()
- self.updateinfo()
-
- def openNetwork(self, widget):
- startallnetwork()
- self.updateinfo()
-
- def signal_icon_name(self, bar, suffix):
- if bar > 75:
+ self.eap_dialog(network, wifi_card, False)
+
+ def disconnect_wifi(self, _widget, wifi_card):
+ """Drop the current wireless association.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ wifi_card (str): wireless interface name.
+ """
+ disconnect_wifi(wifi_card)
+ self.update_info()
+
+ def disable_wifi(self, _widget, wifi_card):
+ """Take a wireless interface down.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ wifi_card (str): wireless interface name.
+ """
+ disable_wifi(wifi_card)
+ self.update_info()
+
+ def enable_wifi(self, _widget, wifi_card):
+ """Bring a wireless interface back up.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ wifi_card (str): wireless interface name.
+ """
+ enable_wifi(wifi_card)
+ self.update_info()
+
+ def enable_card(self, _widget, netcard):
+ """Start a wired interface.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ netcard (str): interface name.
+ """
+ start_network_card(netcard)
+ self.update_info()
+
+ def disable_card(self, _widget, netcard):
+ """Stop a wired interface.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ netcard (str): interface name.
+ """
+ stop_network_card(netcard)
+ self.update_info()
+
+ def connect_wg(self, _widget, wg_device):
+ """Bring a WireGuard tunnel up.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ wg_device (str): the tunnel's configuration name, without .conf.
+ """
+ enable_wg(wg_device)
+ self.update_info()
+
+ def disconnect_wg(self, _widget, wg_device):
+ """Take a WireGuard tunnel down.
+
+ Args:
+ _widget (Gtk.Widget): the menu item, unused.
+ wg_device (str): the tunnel's configuration name, without .conf.
+ """
+ disable_wg(wg_device)
+ self.update_info()
+
+ def signal_icon_name(self, signal, suffix):
+ """Pick the signal icon name for a strength percentage.
+
+ Args:
+ signal (int): signal strength as a percentage.
+ suffix (str): icon name suffix, "-secure" or empty.
+
+ Returns:
+ str: an icon name from the nm-signal set.
+ """
+ if signal > 75:
icon_name = f"nm-signal-100{suffix}"
- elif bar > 50:
+ elif signal > 50:
icon_name = f"nm-signal-75{suffix}"
- elif bar > 25:
+ elif signal > 25:
icon_name = f"nm-signal-50{suffix}"
- elif bar > 5:
+ elif signal > 5:
icon_name = f"nm-signal-25{suffix}"
else:
icon_name = f"nm-signal-00{suffix}"
return icon_name
- def wifi_signal_icon(self, bar, is_secure=False):
+ def wifi_signal_icon(self, signal, is_secure=False):
+ """Build the signal strength image shown next to an SSID.
+
+ Args:
+ signal (int): signal strength as a percentage.
+ is_secure (bool): True to use the padlocked variant.
+
+ Returns:
+ Gtk.Image: a realised image widget.
+ """
img = Gtk.Image()
suffix = ""
if is_secure:
suffix = "-secure"
- icon_name = self.signal_icon_name(bar, suffix)
+ icon_name = self.signal_icon_name(signal, suffix)
img.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
img.show()
return img
- def updateinfo(self):
- if not self.if_running:
- self.if_running = True
- self.cardinfo = networkdictionary()
- defaultcard = self.cardinfo['default']
- default_type = self.network_type(defaultcard)
- GLib.idle_add(self.updatetray, defaultcard, default_type)
- self.wginfo = wg_dictionary()
- self.if_running = False
-
- def updatetray(self, defaultdev, default_type):
- self.updatetrayicon(defaultdev, default_type)
- self.trayStatus(defaultdev)
-
- def updatetrayloop(self):
- while True:
- self.updateinfo()
- sleep(20)
-
- def network_type(self, defaultdev):
- if defaultdev is None:
- return None
- elif 'wlan' in defaultdev:
- return 'wifi'
- else:
- return 'wire'
+ def update_info(self):
+ """
+ Refresh the icon and the tooltip.
- def default_wifi_state(self, defaultdev):
- info = self.cardinfo['cards'][defaultdev]
- if info['state']["connection"] == "Connected":
- ssid = info['state']["ssid"]
- return info['info'][ssid][4]
- else:
- return None
+ The refresh-tick path: only the default-route interface, whether it
+ is up, and its signal. The menu's data is collected on menu open.
+
+ No lock. tray_state() builds the whole dict before it is assigned,
+ so a reader sees the old one or the new one, never a half-built one.
+ """
+ self.traystate = tray_state()
+ GLib.idle_add(self.update_tray)
+
+ def update_tray(self):
+ """Redraw the tray icon and its tooltip from the last light refresh.
+
+ Returns:
+ bool: False, so GLib.idle_add does not call this again.
+ """
+ self.update_tray_icon()
+ self.status_icon.set_tooltip_text(self.traystate['tooltip'])
+ return False
+
+ def update_tray_loop(self):
+ """Refresh the tray for the life of the app.
- def updatetrayicon(self, defaultdev, card_type):
- if card_type is None:
+ Thirty seconds, two to four commands, about 5 ms. It catches only
+ passive change: a cable pulled, a link dropping, signal drifting.
+ Anything the user does refreshes at the end of the action.
+
+ The scan request goes out here too, so the BSS table is warm before
+ anyone opens the menu. It is asked for, never waited on.
+ """
+ while True:
+ self.update_info()
+ request_scan_all()
+ sleep(30)
+
+ def start_icon_animation(self):
+ """Begin cycling the signal icons for a connection attempt.
+
+ Returns:
+ bool: False, so GLib.idle_add does not repeat this.
+ """
+ if self.connecting:
+ return False
+ self.connecting = True
+ self.connect_frame = 0
+ # 300 ms a frame: slow enough to read as deliberate, fast enough to
+ # look busy.
+ GLib.timeout_add(300, self.next_animation_frame)
+ return False
+
+ def next_animation_frame(self):
+ """Draw the next frame of the connecting animation.
+
+ Returns:
+ bool: True to stay on the timer, False once the attempt has
+ finished, which removes the timeout.
+ """
+ if not self.connecting:
+ return False
+ # Filling bars: the signal icons already shipped, cycled the way
+ # NetworkManager does it, so there is no new artwork to install.
+ frames = ('nm-signal-00', 'nm-signal-25', 'nm-signal-50',
+ 'nm-signal-75', 'nm-signal-100')
+ self.status_icon.set_from_icon_name(
+ frames[self.connect_frame % len(frames)]
+ )
+ self.connect_frame += 1
+ return True
+
+ def stop_icon_animation(self):
+ """End the animation and put the real icon back.
+
+ Returns:
+ bool: False, so GLib.idle_add does not repeat this.
+ """
+ self.connecting = False
+ self.update_tray()
+ return False
+
+ def update_tray_icon(self):
+ """Set the tray icon to match the last light refresh.
+
+ Does nothing while a connection attempt is running, so the refresh
+ loop cannot overwrite the animation mid-attempt.
+ """
+ if self.connecting:
+ return
+ kind = self.traystate['kind']
+ if kind is None:
icon_name = 'nm-no-connection'
- elif card_type == 'wire':
+ elif kind == 'wire':
icon_name = 'nm-device-wired'
else:
- wifi_state = self.default_wifi_state(defaultdev)
+ wifi_state = (self.traystate['signal']
+ if self.traystate['connection'] == 'Connected'
+ else None)
if wifi_state is None:
icon_name = 'nm-no-connection'
- elif wifi_state > 80:
- icon_name = 'nm-signal-100'
- elif wifi_state > 60:
- icon_name = 'nm-signal-75'
- elif wifi_state > 40:
- icon_name = 'nm-signal-50'
- elif wifi_state > 20:
- icon_name = 'nm-signal-25'
else:
- icon_name = 'nm-signal-00'
- self.statusIcon.set_from_icon_name(icon_name)
-
- def trayStatus(self, defaultdev):
- self.statusIcon.set_tooltip_text(connectionStatus(defaultdev, self.cardinfo))
-
- def tray(self):
- self.if_running = False
- self.thr = threading.Thread(target=self.updatetrayloop)
- self.thr.setDaemon(True)
+ icon_name = self.signal_icon_name(wifi_state, '')
+ self.status_icon.set_from_icon_name(icon_name)
+
+ def run(self):
+ """Start the background refresh thread and hand control to GTK.
+
+ This is the only place the refresh thread is created. It starts
+ before Gtk.main(), so it is already running by the time any click
+ can arrive, which is why the click handlers do not check on it.
+ """
+ self.thr = threading.Thread(target=self.update_tray_loop)
+ self.thr.daemon = True
self.thr.start()
Gtk.main()
- def close(self, widget):
+ def close(self, _widget):
+ """Hide the passphrase dialog.
+
+ Args:
+ _widget (Gtk.Widget): the Cancel button, unused.
+ """
self.window.hide()
- def add_to_wpa_supplicant(self, widget, ssid_info, card):
+ def add_to_wpa_supplicant(self, _widget, network, card):
+ """
+ Save the typed passphrase and start connecting in the background.
+
+ Args:
+ _widget (Gtk.Widget): the Connect button, unused.
+ network (dict): the network's scan record.
+ card (str): the interface to connect.
+ """
pwd = self.password.get_text()
- self.setup_wpa_supplicant(ssid_info[0], ssid_info, pwd, card)
+ ssid = network['ssid']
+ self.window.hide()
+ wpa_reconfigure(card)
+ # Change the one field, do not delete and rebuild the block.
+ if ssid_is_saved(ssid, card):
+ stored = update_psk_network(ssid, pwd, card)
+ else:
+ stored = save_psk_network(ssid, network['security'], pwd,
+ card) is not None
+ if not stored:
+ self.connection_failed(ssid, card)
+ return
+ self.start_connection(ssid, network, card)
+
+ def start_connection(self, ssid, network, card, enterprise=False):
+ """
+ Begin joining a network on a worker thread.
+
+ The attempt takes up to thirty seconds, so it must not run on the
+ GTK thread.
+
+ Args:
+ ssid (str): the network name to join.
+ network (dict or None): the network's scan record, needed to
+ rebuild the credentials dialog if the key is refused. None
+ for an open network, which has no dialog.
+ card (str): the interface to connect.
+ enterprise (bool): True to reopen the EAP dialog rather than the
+ passphrase one when credentials are refused.
+ """
_thread.start_new_thread(
self.try_to_connect_to_ssid,
- (ssid_info[0], ssid_info, card)
+ (ssid, network, card, enterprise)
)
- self.window.hide()
- def try_to_connect_to_ssid(self, ssid, ssid_info, card):
- if not connectToSsid(ssid, card):
- delete_ssid_wpa_supplicant_config(ssid)
- GLib.idle_add(self.restart_authentication, ssid_info, card)
- else:
- for _ in list(range(60)):
- if nic_status(card) == 'associated':
- self.updateinfo()
- break
- sleep(1)
+ def try_to_connect_to_ssid(self, ssid, network, card, enterprise=False):
+ """
+ Connect, then report the outcome.
+
+ Runs on its own thread, so it must reach the UI through
+ GLib.idle_add.
+
+ Args:
+ ssid (str): the network name.
+ network (dict or None): the network's scan record, or None for
+ an open network.
+ card (str): the interface to connect.
+ enterprise (bool): True to reopen the EAP dialog on refusal.
+ """
+ GLib.idle_add(self.start_icon_animation)
+ try:
+ if not connect_to_ssid(ssid, card):
+ GLib.idle_add(self.connection_failed, ssid, card)
+ return
+ if wait_for_connection(card, ssid):
+ # It works, so now it is worth keeping. Until this point the
+ # network existed only in the daemon.
+ wpa_save_config(card)
else:
- delete_ssid_wpa_supplicant_config(ssid)
- GLib.idle_add(self.restart_authentication, ssid_info, card)
- return
+ self.connect_attempts += 1
+ if network is not None and self.connect_attempts < MAX_ATTEMPTS:
+ GLib.idle_add(self.reopen_credentials_dialog, network, card,
+ enterprise)
+ else:
+ # Out of tries, or an open network with nothing to
+ # retype. Either way the block does not work, so it goes.
+ forget_network(ssid, card)
+ GLib.idle_add(self.connection_failed, ssid, card, True)
+ finally:
+ # select_network disabled the others; leaving them disabled
+ # would strand the card.
+ enable_all_networks(card)
+ # Refresh before the animation ends: stop_icon_animation draws
+ # whatever traystate holds.
+ self.update_info()
+ GLib.idle_add(self.stop_icon_animation)
+
+ def reopen_credentials_dialog(self, network, card, enterprise=False):
+ """
+ Reopen the credentials dialog after an attempt did not connect.
+
+ Args:
+ network (dict): the network's scan record.
+ card (str): the interface to connect.
+ enterprise (bool): True for the EAP dialog, False for the
+ passphrase one.
+ """
+ if enterprise:
+ self.eap_dialog(network, card, True)
+ else:
+ self.passphrase_dialog(network, card, True)
+
+ def connection_failed(self, ssid, card, timed_out=False):
+ """
+ Tell the user the card never joined, without blaming the password.
+
+ Args:
+ ssid (str): the network that was not joined.
+ card (str): the interface that tried.
+ timed_out (bool): True when an attempt ran and the card never
+ joined. False when no attempt was made because the daemon
+ did not answer or refused the network.
+
+ Returns:
+ bool: False, so GLib.idle_add does not call this again.
+ """
+ if timed_out:
+ detail = _(
+ "%(card)s did not associate with %(ssid)s before timing out."
+ "\n\nA weak signal is the usual cause."
+ ) % {'card': card, 'ssid': ssid}
+ else:
+ detail = _("wpa_supplicant did not accept the request on %s.") % card
+ dialog = Gtk.MessageDialog(
+ None, 0, Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE,
+ _("Could not connect to %s") % ssid
+ )
+ dialog.format_secondary_text(detail)
+ dialog.run()
+ dialog.destroy()
+ return False
+
+ def toggle_password_visibility(self, widget):
+ """
+ Show or hide the typed password.
+
+ Args:
+ widget (Gtk.CheckButton): the "Show password" box.
+ """
+ self.password.set_visibility(widget.get_active())
- def restart_authentication(self, ssid_info, card):
- self.Authentication(ssid_info, card, True)
+ def passphrase_dialog(self, network, card, failed):
+ """
+ Build and show the passphrase dialog for one network.
- def on_check(self, widget):
- self.password.set_visibility(widget.get_active())
+ Args:
+ network (dict): the network's scan record.
+ card (str): the interface the password is for.
+ failed (bool): True when a previous attempt did not connect,
+ which only changes the heading.
- def Authentication(self, ssid_info, card, failed):
+ Returns:
+ str: 'Done', kept because the caller has always ignored it.
+ """
+ ssid = network['ssid']
self.window = Gtk.Window()
self.window.set_title(_("Wi-Fi Network Authentication Required"))
self.window.set_border_width(0)
@@ -429,18 +880,18 @@ def Authentication(self, ssid_info, card, failed):
box2.set_border_width(10)
box1.pack_start(box2, True, True, 0)
box2.show()
- # Creating MBR or GPT drive
if failed:
- title = _(f"{ssid_info[0]} Wi-Fi Network Authentication failed")
+ # A refused key and a card that never answered look identical
+ # on this driver, so the wording blames neither.
+ title = _("Could not connect to %s. Try again.") % ssid
else:
- title = _(f"Authentication required by {ssid_info[0]} Wi-Fi Network")
- label = Gtk.Label(f"{title}")
- label.set_use_markup(True)
+ title = _("Authentication required by %s Wi-Fi Network") % ssid
+ label = _heading_label(title)
pwd_label = Gtk.Label(_("Password:"))
self.password = Gtk.Entry()
self.password.set_visibility(False)
check = Gtk.CheckButton(_("Show password"))
- check.connect("toggled", self.on_check)
+ check.connect("toggled", self.toggle_password_visibility)
table = Gtk.Table(1, 2, True)
table.attach(label, 0, 5, 0, 1)
table.attach(pwd_label, 1, 2, 2, 3)
@@ -455,7 +906,7 @@ def Authentication(self, ssid_info, card, failed):
cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
cancel.connect("clicked", self.close)
connect = Gtk.Button(stock=Gtk.STOCK_CONNECT)
- connect.connect("clicked", self.add_to_wpa_supplicant, ssid_info, card)
+ connect.connect("clicked", self.add_to_wpa_supplicant, network, card)
table = Gtk.Table(1, 2, True)
table.set_col_spacings(10)
table.attach(connect, 4, 5, 0, 1)
@@ -464,118 +915,101 @@ def Authentication(self, ssid_info, card, failed):
self.window.show_all()
return 'Done'
- def setup_wpa_supplicant(self, ssid, ssid_info, pwd, card):
- # Determine caps string - handle extended ssid_info format
- # Index 7 contains the full caps_string (e.g., "RSN HTCAP WME...")
- # Index 6 is the short CAPS (e.g., "EPS") which doesn't contain RSN/WPA
- caps_string = ssid_info[7] if len(ssid_info) > 7 else ssid_info[-1]
- if 'RSN' in caps_string:
- # /etc/wpa_supplicant.conf written by networkmgr
- ws = '\nnetwork={'
- ws += f'\n ssid="{ssid}"'
- ws += '\n key_mgmt=WPA-PSK'
- ws += '\n proto=RSN'
- ws += f'\n psk="{pwd}"\n'
- ws += '}\n'
- elif 'WPA' in caps_string:
- ws = '\nnetwork={'
- ws += f'\n ssid="{ssid}"'
- ws += '\n key_mgmt=WPA-PSK'
- ws += '\n proto=WPA'
- ws += f'\n psk="{pwd}"\n'
- ws += '}\n'
- else:
- ws = '\nnetwork={'
- ws += f'\n ssid="{ssid}"'
- ws += '\n key_mgmt=NONE'
- ws += '\n wep_tx_keyidx=0'
- ws += f'\n wep_key0={pwd}\n'
- ws += '}\n'
- import os
- old_umask = os.umask(0o077)
- try:
- with open("/etc/wpa_supplicant.conf", 'a') as wsf:
- wsf.write(ws)
- finally:
- os.umask(old_umask)
-
- def Open_Wpa_Supplicant(self, ssid, card):
- ws = '\nnetwork={'
- ws += f'\n ssid="{ssid}"'
- ws += '\n key_mgmt=NONE\n}\n'
- import os
- old_umask = os.umask(0o077)
- try:
- with open("/etc/wpa_supplicant.conf", 'a') as wsf:
- wsf.write(ws)
- finally:
- os.umask(old_umask)
+ def add_enterprise_to_wpa_supplicant(self, _widget, network, card):
+ """
+ Collect the EAP form, save it, and start connecting in the background.
- def add_enterprise_to_wpa_supplicant(self, widget, ssid_info, card):
- """Handle enterprise authentication form submission."""
+ Args:
+ _widget (Gtk.Widget): the Connect button, unused.
+ network (dict): the network's scan record.
+ card (str): the interface to connect.
+ """
eap_config = {
'eap_method': self.eap_method_combo.get_active_text(),
'identity': self.identity_entry.get_text(),
'password': self.eap_password.get_text(),
- 'phase2': self.phase2_combo.get_active_text() if hasattr(self, 'phase2_combo') else 'MSCHAPV2',
- 'anonymous_identity': self.anon_identity_entry.get_text() if hasattr(self, 'anon_identity_entry') else '',
+ 'phase2': self.phase2_combo.get_active_text() if self.phase2_combo is not None else 'MSCHAPV2',
+ 'anonymous_identity': self.anon_identity_entry.get_text() if self.anon_identity_entry is not None else '',
}
# Get CA certificate path
- if hasattr(self, 'ca_cert_chooser'):
+ if self.ca_cert_chooser is not None:
ca_file = self.ca_cert_chooser.get_filename()
if ca_file:
eap_config['ca_cert'] = ca_file
# For TLS, get client certificate and key
if eap_config['eap_method'] == 'TLS':
- if hasattr(self, 'client_cert_chooser'):
+ if self.client_cert_chooser is not None:
client_cert = self.client_cert_chooser.get_filename()
if client_cert:
eap_config['client_cert'] = client_cert
- if hasattr(self, 'private_key_chooser'):
+ if self.private_key_chooser is not None:
private_key = self.private_key_chooser.get_filename()
if private_key:
eap_config['private_key'] = private_key
- if hasattr(self, 'private_key_passwd'):
+ if self.private_key_passwd is not None:
eap_config['private_key_passwd'] = self.private_key_passwd.get_text()
- write_eap_config(ssid_info[0], eap_config)
- _thread.start_new_thread(
- self.try_to_connect_to_ssid,
- (ssid_info[0], ssid_info, card)
- )
self.eap_window.hide()
+ wpa_reconfigure(card)
+ if save_eap_network(network['ssid'], eap_config, card) is None:
+ self.connection_failed(network['ssid'], card)
+ return
+ self.start_connection(network['ssid'], network, card, enterprise=True)
def on_eap_method_changed(self, combo):
- """Update dialog fields based on selected EAP method."""
- method = combo.get_active_text()
+ """Show only the fields the chosen EAP method uses.
- # Show/hide TLS-specific fields
- tls_mode = method == 'TLS'
- if hasattr(self, 'client_cert_box'):
- self.client_cert_box.set_visible(tls_mode)
- if hasattr(self, 'private_key_box'):
- self.private_key_box.set_visible(tls_mode)
- if hasattr(self, 'private_key_passwd_box'):
- self.private_key_passwd_box.set_visible(tls_mode)
+ Each row is a label and a field, hidden as a pair. Hiding the field
+ alone leaves its label naming an empty cell.
- # Show/hide password field (not needed for TLS)
- if hasattr(self, 'password_box'):
- self.password_box.set_visible(not tls_mode)
+ Args:
+ combo (Gtk.ComboBoxText): the EAP method chooser.
+ """
+ method = combo.get_active_text()
+ tls = method == 'TLS'
+ shown = {
+ 'phase2': method in ('PEAP', 'TTLS'),
+ 'password': not tls,
+ 'client_cert': tls,
+ 'private_key': tls,
+ 'private_key_passwd': tls,
+ }
+ for name, visible in shown.items():
+ for widget in self.eap_rows.get(name, ()):
+ widget.set_visible(visible)
- # Show/hide phase2 (only for PEAP/TTLS)
- if hasattr(self, 'phase2_box'):
- self.phase2_box.set_visible(method in ('PEAP', 'TTLS'))
+ def close_eap_window(self, _widget):
+ """Hide the enterprise credentials dialog.
- def close_eap_window(self, widget):
+ Args:
+ _widget (Gtk.Widget): the Cancel button, unused.
+ """
self.eap_window.hide()
def on_eap_password_check(self, widget):
+ """Show or hide the typed EAP password.
+
+ Args:
+ widget (Gtk.CheckButton): the "show password" box.
+ """
self.eap_password.set_visibility(widget.get_active())
- def EnterpriseAuthentication(self, ssid_info, card, failed):
- """Create authentication dialog for WPA-Enterprise networks."""
+ def eap_dialog(self, network, card, failed):
+ """
+ Build and show the EAP credentials dialog for one network.
+
+ Args:
+ network (dict): the network's scan record.
+ card (str): the interface the credentials are for.
+ failed (bool): True to title the window as a failed attempt.
+
+ Returns:
+ str: 'Done', kept because the caller has always ignored it.
+ """
+ ssid = network['ssid']
+ self.eap_rows = {}
self.eap_window = Gtk.Window()
self.eap_window.set_title(_("Enterprise Wi-Fi Authentication"))
self.eap_window.set_border_width(10)
@@ -586,16 +1020,13 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
# Title
if failed:
- title_text = _("%s Enterprise Authentication Failed") % ssid_info[0]
+ title_text = _("%s Enterprise Authentication Failed") % ssid
else:
- title_text = _("Enterprise Authentication for %s") % ssid_info[0]
- title_label = Gtk.Label()
- title_label.set_markup(f"{title_text}")
+ title_text = _("Enterprise Authentication for %s") % ssid
+ title_label = _heading_label(title_text)
main_box.pack_start(title_label, False, False, 5)
- # Security info
- security_type = ssid_info[7] if len(ssid_info) > 7 else "WPA2-EAP"
- security_label = Gtk.Label(_("Security: %s") % security_type)
+ security_label = Gtk.Label(_("Security: %s") % network['security'])
main_box.pack_start(security_label, False, False, 0)
# Grid for form fields
@@ -619,7 +1050,7 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
row += 1
# Phase 2 Authentication (inner method)
- self.phase2_box = Gtk.HBox(spacing=5)
+ phase2_box = Gtk.HBox(spacing=5)
phase2_label = Gtk.Label(_("Inner Auth:"))
phase2_label.set_halign(Gtk.Align.END)
grid.attach(phase2_label, 0, row, 1, 1)
@@ -627,8 +1058,9 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
for method in PHASE2_METHODS:
self.phase2_combo.append_text(method)
self.phase2_combo.set_active(0) # Default to MSCHAPV2
- self.phase2_box.pack_start(self.phase2_combo, True, True, 0)
- grid.attach(self.phase2_box, 1, row, 2, 1)
+ phase2_box.pack_start(self.phase2_combo, True, True, 0)
+ grid.attach(phase2_box, 1, row, 2, 1)
+ self.eap_rows['phase2'] = (phase2_label, phase2_box)
row += 1
# Identity (Username)
@@ -650,7 +1082,7 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
row += 1
# Password
- self.password_box = Gtk.VBox()
+ password_box = Gtk.VBox()
pwd_label = Gtk.Label(_("Password:"))
pwd_label.set_halign(Gtk.Align.END)
grid.attach(pwd_label, 0, row, 1, 1)
@@ -662,8 +1094,9 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
show_pwd_check = Gtk.CheckButton(_("Show"))
show_pwd_check.connect("toggled", self.on_eap_password_check)
pwd_hbox.pack_start(show_pwd_check, False, False, 0)
- self.password_box.pack_start(pwd_hbox, True, True, 0)
- grid.attach(self.password_box, 1, row, 2, 1)
+ password_box.pack_start(pwd_hbox, True, True, 0)
+ grid.attach(password_box, 1, row, 2, 1)
+ self.eap_rows['password'] = (pwd_label, password_box)
row += 1
# CA Certificate
@@ -691,7 +1124,7 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
# TLS-specific fields (hidden by default)
# Client Certificate
- self.client_cert_box = Gtk.HBox(spacing=5)
+ client_cert_box = Gtk.HBox(spacing=5)
client_cert_label = Gtk.Label(_("Client Cert:"))
client_cert_label.set_halign(Gtk.Align.END)
grid.attach(client_cert_label, 0, row, 1, 1)
@@ -700,13 +1133,13 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
action=Gtk.FileChooserAction.OPEN
)
self.client_cert_chooser.add_filter(ca_filter)
- self.client_cert_box.pack_start(self.client_cert_chooser, True, True, 0)
- grid.attach(self.client_cert_box, 1, row, 2, 1)
- self.client_cert_box.set_visible(False)
+ client_cert_box.pack_start(self.client_cert_chooser, True, True, 0)
+ grid.attach(client_cert_box, 1, row, 2, 1)
+ self.eap_rows['client_cert'] = (client_cert_label, client_cert_box)
row += 1
# Private Key
- self.private_key_box = Gtk.HBox(spacing=5)
+ private_key_box = Gtk.HBox(spacing=5)
key_label = Gtk.Label(_("Private Key:"))
key_label.set_halign(Gtk.Align.END)
grid.attach(key_label, 0, row, 1, 1)
@@ -720,21 +1153,22 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
key_filter.add_pattern("*.key")
key_filter.add_pattern("*.p12")
self.private_key_chooser.add_filter(key_filter)
- self.private_key_box.pack_start(self.private_key_chooser, True, True, 0)
- grid.attach(self.private_key_box, 1, row, 2, 1)
- self.private_key_box.set_visible(False)
+ private_key_box.pack_start(self.private_key_chooser, True, True, 0)
+ grid.attach(private_key_box, 1, row, 2, 1)
+ self.eap_rows['private_key'] = (key_label, private_key_box)
row += 1
# Private Key Password
- self.private_key_passwd_box = Gtk.HBox(spacing=5)
+ private_key_passwd_box = Gtk.HBox(spacing=5)
key_pwd_label = Gtk.Label(_("Key Password:"))
key_pwd_label.set_halign(Gtk.Align.END)
grid.attach(key_pwd_label, 0, row, 1, 1)
self.private_key_passwd = Gtk.Entry()
self.private_key_passwd.set_visibility(False)
- self.private_key_passwd_box.pack_start(self.private_key_passwd, True, True, 0)
- grid.attach(self.private_key_passwd_box, 1, row, 2, 1)
- self.private_key_passwd_box.set_visible(False)
+ private_key_passwd_box.pack_start(self.private_key_passwd, True, True, 0)
+ grid.attach(private_key_passwd_box, 1, row, 2, 1)
+ self.eap_rows['private_key_passwd'] = (key_pwd_label,
+ private_key_passwd_box)
row += 1
# Buttons
@@ -747,7 +1181,7 @@ def EnterpriseAuthentication(self, ssid_info, card, failed):
button_box.pack_start(cancel_btn, False, False, 0)
connect_btn = Gtk.Button(stock=Gtk.STOCK_CONNECT)
- connect_btn.connect("clicked", self.add_enterprise_to_wpa_supplicant, ssid_info, card)
+ connect_btn.connect("clicked", self.add_enterprise_to_wpa_supplicant, network, card)
button_box.pack_start(connect_btn, False, False, 0)
self.eap_window.show_all()
diff --git a/NetworkMgr/wg_api.py b/NetworkMgr/wg_api.py
index e762d1e..3d9b07f 100644
--- a/NetworkMgr/wg_api.py
+++ b/NetworkMgr/wg_api.py
@@ -1,29 +1,60 @@
-#!/usr/local/bin/python3.11
+#!/usr/bin/env python
+"""WireGuard tunnel discovery and control.
+
+Lists the tunnel configurations under $PREFIX/etc/wireguard, reports which
+are running, and brings them up or down through the wireguard rc service.
+"""
+
+import os
+import sys
from platform import system
-from subprocess import PIPE, run, os
+from subprocess import PIPE, run
PREFIX = '/usr/local' if system() == 'FreeBSD' else sys.prefix
WG_CONFIG_PATH = f'{PREFIX}/etc/wireguard/'
+
def wg_service_state():
- """Function returns the WireGuard service status."""
- result = run(['service', 'wireguard', 'rcvar'], stdout=PIPE, stderr=PIPE, check=False)
- out = result.stdout.decode('utf-8')
+ """Report whether rc is set to manage the WireGuard tunnels.
+
+ Asked through sysrc rather than `service wireguard rcvar`, which answers
+ the same question but wraps it in four lines of prose that have to be
+ parsed, and quotes the value so every caller has to compare against
+ '"NO"'. sysrc gives the bare value.
+
+ Returns:
+ str: 'YES' or 'NO'. A variable set nowhere reads as 'NO', which is
+ the rc script's own default: line 69 of
+ /usr/local/etc/rc.d/wireguard is `: ${wireguard_enable="NO"}`,
+ and sysrc reports an unset variable as an error rather than a
+ value.
+ """
+ result = run(['sysrc', '-n', 'wireguard_enable'],
+ stdout=PIPE, stderr=PIPE, check=False, text=True)
+ if result.returncode != 0:
+ return 'NO'
+ return result.stdout.strip() or 'NO'
- state = 'Unknown'
- for line in out.splitlines():
- if "wireguard_enable=" in line:
- state = line.split('=')[1].strip()
- break
+def wg_dictionary(service_state):
+ """Collect the WireGuard tunnels and their state.
- return state
+ The service state is passed in rather than read here. Asking rc for it
+ costs about 13 ms of shell, against under 1 ms for everything else in
+ this function, and `wireguard_enable` only changes when someone edits
+ rc.conf. Reading it once and handing it down keeps the tray refresh
+ cheap.
-def wg_dictionary():
- """Function returns the WireGuard configurations."""
+ Args:
+ service_state (str): 'YES' or 'NO' from wg_service_state().
+
+ Returns:
+ dict: 'service' as given, 'default', and 'configs' mapping each
+ tunnel device to its 'state' and its 'info' display name.
+ """
maindictionary = {
- 'service': wg_service_state(),
+ 'service': service_state,
'default': '',
}
configs = {}
@@ -46,13 +77,24 @@ def wg_dictionary():
maindictionary['configs'] = configs
return maindictionary
+
def disable_wg(wgconfig):
- """Function disable the specified WireGuard configuration (device)."""
- run(f'wg-quick down {wgconfig}', shell=True, check=False)
+ """Take the specified WireGuard configuration (device) down.
+
+ Args:
+ wgconfig (str): the tunnel's configuration name, without .conf.
+ """
+ run(['wg-quick', 'down', wgconfig], check=False)
+
def enable_wg(wgconfig):
- """Function enable the specified WireGuard configuration (device)."""
- run(f'wg-quick up {wgconfig}', shell=True, check=False)
+ """Bring the specified WireGuard configuration (device) up.
+
+ Args:
+ wgconfig (str): the tunnel's configuration name, without .conf.
+ """
+ run(['wg-quick', 'up', wgconfig], check=False)
+
def wg_status(wgconfig):
"""Function returning the WireGuard configuration (device) is connected or not."""
diff --git a/networkmgr b/networkmgr
index b804412..6675df0 100755
--- a/networkmgr
+++ b/networkmgr
@@ -1,8 +1,8 @@
#!/usr/bin/env python
import signal
-from NetworkMgr.trayicon import trayIcon
+from NetworkMgr.trayicon import TrayIcon
signal.signal(signal.SIGINT, signal.SIG_DFL)
-trayIcon().tray()
+TrayIcon().run()
diff --git a/networkmgr_configuration b/networkmgr_configuration
index 9b57566..4282571 100755
--- a/networkmgr_configuration
+++ b/networkmgr_configuration
@@ -5,4 +5,4 @@ from NetworkMgr import configuration
signal.signal(signal.SIGINT, signal.SIG_DFL)
-configuration.network_card_configuration_window()
+configuration.open_default_configuration()
diff --git a/po/de.po b/po/de.po
index 047fd63..4175d6a 100644
--- a/po/de.po
+++ b/po/de.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-12-14 17:25+0000\n"
+"POT-Creation-Date: 2026-09-11 21:33-0300\n"
"PO-Revision-Date: 2023-12-14 17:25+0000\n"
"Last-Translator: Joshua Hoffmann \n"
"Language-Team: Language locale/de\n"
@@ -17,97 +17,236 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: src/trayicon.py:67
+#: NetworkMgr/trayicon.py:173
+msgid "WireGuard VPN"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:182
+#, fuzzy, python-format
+msgid "%s Connected"
+msgstr "WLAN %s verbunden"
+
+#: NetworkMgr/trayicon.py:185 NetworkMgr/trayicon.py:219
+#: NetworkMgr/trayicon.py:230
+#, fuzzy, python-format
+msgid "Disable %s"
+msgstr "Deaktivieren"
+
+#: NetworkMgr/trayicon.py:189
+#, fuzzy, python-format
+msgid "%s Disconnected"
+msgstr "WLAN %s getrennt"
+
+#: NetworkMgr/trayicon.py:192 NetworkMgr/trayicon.py:212
+msgid "Enable"
+msgstr "Aktivieren"
+
+#: NetworkMgr/trayicon.py:198
msgid "Ethernet Network"
msgstr "Ethernet-Netzwerk"
-#: src/trayicon.py:78
+#: NetworkMgr/trayicon.py:209
+#, fuzzy, python-format
+msgid "Wired %s Disabled"
+msgstr "WLAN %s deaktiviert"
+
+#: NetworkMgr/trayicon.py:216
#, python-format
msgid "Wired %s Connected"
msgstr "Kabel an %s angeschlossen"
-#: src/trayicon.py:81
-msgid "Disable"
-msgstr "Deaktivieren"
+#: NetworkMgr/trayicon.py:223 NetworkMgr/trayicon.py:234
+#: NetworkMgr/trayicon.py:281
+#, python-format
+msgid "Configure %s"
+msgstr ""
-#: src/trayicon.py:86
+#: NetworkMgr/trayicon.py:227
#, python-format
msgid "Wired %s Disconnected"
msgstr "Kabel von %s getrennt"
-#: src/trayicon.py:89
-msgid "Enable"
-msgstr "Aktivieren"
-
-#: src/trayicon.py:93
+#: NetworkMgr/trayicon.py:238
#, python-format
msgid "Wired %s Unplug"
msgstr "Kabel von %s abgesteckt"
-#: src/trayicon.py:101
+#: NetworkMgr/trayicon.py:246
#, python-format
msgid "WiFi %s Disabled"
msgstr "WLAN %s deaktiviert"
-#: src/trayicon.py:104
+#: NetworkMgr/trayicon.py:249
#, python-format
msgid "Enable Wifi %s"
msgstr "WLAN %s aktivieren"
-#: src/trayicon.py:109
+#: NetworkMgr/trayicon.py:254
#, python-format
msgid "WiFi %s Disconnected"
msgstr "WLAN %s getrennt"
-#: src/trayicon.py:113 src/trayicon.py:131
+#: NetworkMgr/trayicon.py:259 NetworkMgr/trayicon.py:278
#, python-format
msgid "Disable Wifi %s"
msgstr "WLAN %s deaktivieren"
-#: src/trayicon.py:119
+#: NetworkMgr/trayicon.py:265
#, python-format
msgid "WiFi %s Connected"
msgstr "WLAN %s verbunden"
-#: src/trayicon.py:125
+#: NetworkMgr/trayicon.py:271
#, python-format
msgid "Disconnect from %s"
msgstr "%s trennen"
-#: src/trayicon.py:139
-msgid "Enable Networking"
-msgstr "Netzwerk aktivieren"
-
-#: src/trayicon.py:143
-msgid "Disable Networking"
+#: NetworkMgr/trayicon.py:287
+#, fuzzy
+msgid "Restart Networking"
msgstr "Netzwerkverbindungen deaktivieren"
-#: src/trayicon.py:148
+#: NetworkMgr/trayicon.py:290
msgid "Close Network Manager"
msgstr "Network Manager schließen"
-#: src/trayicon.py:156
+#: NetworkMgr/trayicon.py:312
+#, python-format
+msgid "%s (WPA3 only)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:349
msgid "Available Connections"
msgstr "Verfügbare Verbindungen"
-#: src/trayicon.py:364
+#: NetworkMgr/trayicon.py:367
+#, fuzzy
+msgid "Forget Network"
+msgstr "Ethernet-Netzwerk"
+
+#: NetworkMgr/trayicon.py:835
+#, python-format
+msgid ""
+"%(card)s did not associate with %(ssid)s before timing out.\n"
+"\n"
+"A weak signal is the usual cause."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:839
+#, python-format
+msgid "wpa_supplicant did not accept the request on %s."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:842
+#, python-format
+msgid "Could not connect to %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:873
msgid "Wi-Fi Network Authentication Required"
msgstr "WLAN erfordert Legitimierung"
-#: src/trayicon.py:376
-#, python-brace-format
-msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
-msgstr "Legitimierung für WLAN {ssid_info[0]} fehlgeschlagen"
+#: NetworkMgr/trayicon.py:886
+#, python-format
+msgid "Could not connect to %s. Try again."
+msgstr ""
-#: src/trayicon.py:378
-#, python-brace-format
-msgid "Authentication required by {ssid_info[0]} Wi-Fi Network"
+#: NetworkMgr/trayicon.py:888
+#, fuzzy, python-format
+msgid "Authentication required by %s Wi-Fi Network"
msgstr "Legitimierung für WLAN {ssid_info[0]} erforderlich"
-#: src/trayicon.py:381
+#: NetworkMgr/trayicon.py:890 NetworkMgr/trayicon.py:1086
msgid "Password:"
msgstr "Passwort:"
-#: src/trayicon.py:384
+#: NetworkMgr/trayicon.py:893
msgid "Show password"
msgstr "Passwort anzeigen"
+
+#: NetworkMgr/trayicon.py:1014
+msgid "Enterprise Wi-Fi Authentication"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1023
+#, fuzzy, python-format
+msgid "%s Enterprise Authentication Failed"
+msgstr "WLAN erfordert Legitimierung"
+
+#: NetworkMgr/trayicon.py:1025
+#, python-format
+msgid "Enterprise Authentication for %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1029
+#, python-format
+msgid "Security: %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1041
+msgid "EAP Method:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1054
+msgid "Inner Auth:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1067
+msgid "Username:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1076
+msgid "Anonymous ID:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1080
+msgid "Optional - for privacy"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1094
+msgid "Show"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1103
+msgid "CA Certificate:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1108
+msgid "Select CA Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1116
+msgid "Certificates (*.pem, *.crt, *.cer)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1128
+msgid "Client Cert:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1132
+msgid "Select Client Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1143
+msgid "Private Key:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1147
+msgid "Select Private Key"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1151
+msgid "Key files (*.pem, *.key, *.p12)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1163
+#, fuzzy
+msgid "Key Password:"
+msgstr "Passwort:"
+
+#~ msgid "Enable Networking"
+#~ msgstr "Netzwerk aktivieren"
+
+#, python-brace-format
+#~ msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
+#~ msgstr "Legitimierung für WLAN {ssid_info[0]} fehlgeschlagen"
diff --git a/po/nb_NO.po b/po/nb_NO.po
index 5052424..3b57936 100644
--- a/po/nb_NO.po
+++ b/po/nb_NO.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-12-14 17:25+0000\n"
+"POT-Creation-Date: 2026-09-11 21:33-0300\n"
"PO-Revision-Date: 2023-12-14 17:25+0000\n"
"Last-Translator: Joshua Hoffmann \n"
"Language-Team: Language locale/nb_NO\n"
@@ -17,97 +17,236 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: src/trayicon.py:67
+#: NetworkMgr/trayicon.py:173
+msgid "WireGuard VPN"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:182
+#, fuzzy, python-format
+msgid "%s Connected"
+msgstr "WiFi %s tillkoblet"
+
+#: NetworkMgr/trayicon.py:185 NetworkMgr/trayicon.py:219
+#: NetworkMgr/trayicon.py:230
+#, fuzzy, python-format
+msgid "Disable %s"
+msgstr "Deaktiver"
+
+#: NetworkMgr/trayicon.py:189
+#, fuzzy, python-format
+msgid "%s Disconnected"
+msgstr "WiFi %s frakoblet"
+
+#: NetworkMgr/trayicon.py:192 NetworkMgr/trayicon.py:212
+msgid "Enable"
+msgstr "Aktiver"
+
+#: NetworkMgr/trayicon.py:198
msgid "Ethernet Network"
msgstr "Ethernet-nettverk"
-#: src/trayicon.py:78
+#: NetworkMgr/trayicon.py:209
+#, fuzzy, python-format
+msgid "Wired %s Disabled"
+msgstr "WiFi %s deaktivert"
+
+#: NetworkMgr/trayicon.py:216
#, python-format
msgid "Wired %s Connected"
msgstr "Kablet %s er tilkoblet"
-#: src/trayicon.py:81
-msgid "Disable"
-msgstr "Deaktiver"
+#: NetworkMgr/trayicon.py:223 NetworkMgr/trayicon.py:234
+#: NetworkMgr/trayicon.py:281
+#, python-format
+msgid "Configure %s"
+msgstr ""
-#: src/trayicon.py:86
+#: NetworkMgr/trayicon.py:227
#, python-format
msgid "Wired %s Disconnected"
msgstr "Kablet forbindelse %s er frakoblet"
-#: src/trayicon.py:89
-msgid "Enable"
-msgstr "Aktiver"
-
-#: src/trayicon.py:93
+#: NetworkMgr/trayicon.py:238
#, python-format
msgid "Wired %s Unplug"
msgstr "Kablet forbindelse %s er frakoblet"
-#: src/trayicon.py:101
+#: NetworkMgr/trayicon.py:246
#, python-format
msgid "WiFi %s Disabled"
msgstr "WiFi %s deaktivert"
-#: src/trayicon.py:104
+#: NetworkMgr/trayicon.py:249
#, python-format
msgid "Enable Wifi %s"
msgstr "Aktiver WiFi %s"
-#: src/trayicon.py:109
+#: NetworkMgr/trayicon.py:254
#, python-format
msgid "WiFi %s Disconnected"
msgstr "WiFi %s frakoblet"
-#: src/trayicon.py:113 src/trayicon.py:131
+#: NetworkMgr/trayicon.py:259 NetworkMgr/trayicon.py:278
#, python-format
msgid "Disable Wifi %s"
msgstr "Deaktiver WiFi %s"
-#: src/trayicon.py:119
+#: NetworkMgr/trayicon.py:265
#, python-format
msgid "WiFi %s Connected"
msgstr "WiFi %s tillkoblet"
-#: src/trayicon.py:125
+#: NetworkMgr/trayicon.py:271
#, python-format
msgid "Disconnect from %s"
msgstr "Koble fra %s"
-#: src/trayicon.py:139
-msgid "Enable Networking"
-msgstr "Aktiver nettverksfunksjon"
-
-#: src/trayicon.py:143
-msgid "Disable Networking"
+#: NetworkMgr/trayicon.py:287
+#, fuzzy
+msgid "Restart Networking"
msgstr "Deaktiver nettverkstilkoblinger"
-#: src/trayicon.py:148
+#: NetworkMgr/trayicon.py:290
msgid "Close Network Manager"
msgstr "Lukk Network Manager"
-#: src/trayicon.py:156
+#: NetworkMgr/trayicon.py:312
+#, python-format
+msgid "%s (WPA3 only)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:349
msgid "Available Connections"
msgstr "Tilgjengelige forbindelser"
-#: src/trayicon.py:364
+#: NetworkMgr/trayicon.py:367
+#, fuzzy
+msgid "Forget Network"
+msgstr "Ethernet-nettverk"
+
+#: NetworkMgr/trayicon.py:835
+#, python-format
+msgid ""
+"%(card)s did not associate with %(ssid)s before timing out.\n"
+"\n"
+"A weak signal is the usual cause."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:839
+#, python-format
+msgid "wpa_supplicant did not accept the request on %s."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:842
+#, python-format
+msgid "Could not connect to %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:873
msgid "Wi-Fi Network Authentication Required"
msgstr "Wi-Fi-nettverksautentisering kreves"
-#: src/trayicon.py:376
-#, python-brace-format
-msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
-msgstr "{ssid_info[0]} Wi-Fi-nettverksautentisering mislyktes"
+#: NetworkMgr/trayicon.py:886
+#, python-format
+msgid "Could not connect to %s. Try again."
+msgstr ""
-#: src/trayicon.py:378
-#, python-brace-format
-msgid "Authentication required by {ssid_info[0]} Wi-Fi Network"
+#: NetworkMgr/trayicon.py:888
+#, fuzzy, python-format
+msgid "Authentication required by %s Wi-Fi Network"
msgstr "Autentisering kreves av {ssid_info[0]} Wi-Fi-nettverk"
-#: src/trayicon.py:381
+#: NetworkMgr/trayicon.py:890 NetworkMgr/trayicon.py:1086
msgid "Password:"
msgstr "Passord:"
-#: src/trayicon.py:384
+#: NetworkMgr/trayicon.py:893
msgid "Show password"
msgstr "Vis passord"
+
+#: NetworkMgr/trayicon.py:1014
+msgid "Enterprise Wi-Fi Authentication"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1023
+#, fuzzy, python-format
+msgid "%s Enterprise Authentication Failed"
+msgstr "Wi-Fi-nettverksautentisering kreves"
+
+#: NetworkMgr/trayicon.py:1025
+#, python-format
+msgid "Enterprise Authentication for %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1029
+#, python-format
+msgid "Security: %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1041
+msgid "EAP Method:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1054
+msgid "Inner Auth:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1067
+msgid "Username:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1076
+msgid "Anonymous ID:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1080
+msgid "Optional - for privacy"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1094
+msgid "Show"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1103
+msgid "CA Certificate:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1108
+msgid "Select CA Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1116
+msgid "Certificates (*.pem, *.crt, *.cer)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1128
+msgid "Client Cert:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1132
+msgid "Select Client Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1143
+msgid "Private Key:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1147
+msgid "Select Private Key"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1151
+msgid "Key files (*.pem, *.key, *.p12)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1163
+#, fuzzy
+msgid "Key Password:"
+msgstr "Passord:"
+
+#~ msgid "Enable Networking"
+#~ msgstr "Aktiver nettverksfunksjon"
+
+#, python-brace-format
+#~ msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
+#~ msgstr "{ssid_info[0]} Wi-Fi-nettverksautentisering mislyktes"
diff --git a/po/networkmgr.pot b/po/networkmgr.pot
index 319dc7d..8c871c0 100644
--- a/po/networkmgr.pot
+++ b/po/networkmgr.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-08-29 15:54+0800\n"
+"POT-Creation-Date: 2026-09-11 21:33-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -17,97 +17,226 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
-#: src/trayicon.py:67
-msgid "Ethernet Network"
+#: NetworkMgr/trayicon.py:173
+msgid "WireGuard VPN"
msgstr ""
-#: src/trayicon.py:78
+#: NetworkMgr/trayicon.py:182
#, python-format
-msgid "Wired %s Connected"
+msgid "%s Connected"
msgstr ""
-#: src/trayicon.py:81
-msgid "Disable"
+#: NetworkMgr/trayicon.py:185 NetworkMgr/trayicon.py:219
+#: NetworkMgr/trayicon.py:230
+#, python-format
+msgid "Disable %s"
msgstr ""
-#: src/trayicon.py:86
+#: NetworkMgr/trayicon.py:189
#, python-format
-msgid "Wired %s Disconnected"
+msgid "%s Disconnected"
msgstr ""
-#: src/trayicon.py:89
+#: NetworkMgr/trayicon.py:192 NetworkMgr/trayicon.py:212
msgid "Enable"
msgstr ""
-#: src/trayicon.py:93
+#: NetworkMgr/trayicon.py:198
+msgid "Ethernet Network"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:209
+#, python-format
+msgid "Wired %s Disabled"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:216
+#, python-format
+msgid "Wired %s Connected"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:223 NetworkMgr/trayicon.py:234
+#: NetworkMgr/trayicon.py:281
+#, python-format
+msgid "Configure %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:227
+#, python-format
+msgid "Wired %s Disconnected"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:238
#, python-format
msgid "Wired %s Unplug"
msgstr ""
-#: src/trayicon.py:101
+#: NetworkMgr/trayicon.py:246
#, python-format
msgid "WiFi %s Disabled"
msgstr ""
-#: src/trayicon.py:104
+#: NetworkMgr/trayicon.py:249
#, python-format
msgid "Enable Wifi %s"
msgstr ""
-#: src/trayicon.py:109
+#: NetworkMgr/trayicon.py:254
#, python-format
msgid "WiFi %s Disconnected"
msgstr ""
-#: src/trayicon.py:113 src/trayicon.py:131
+#: NetworkMgr/trayicon.py:259 NetworkMgr/trayicon.py:278
#, python-format
msgid "Disable Wifi %s"
msgstr ""
-#: src/trayicon.py:119
+#: NetworkMgr/trayicon.py:265
#, python-format
msgid "WiFi %s Connected"
msgstr ""
-#: src/trayicon.py:125
+#: NetworkMgr/trayicon.py:271
#, python-format
msgid "Disconnect from %s"
msgstr ""
-#: src/trayicon.py:139
-msgid "Enable Networking"
+#: NetworkMgr/trayicon.py:287
+msgid "Restart Networking"
msgstr ""
-#: src/trayicon.py:143
-msgid "Disable Networking"
+#: NetworkMgr/trayicon.py:290
+msgid "Close Network Manager"
msgstr ""
-#: src/trayicon.py:148
-msgid "Close Network Manager"
+#: NetworkMgr/trayicon.py:312
+#, python-format
+msgid "%s (WPA3 only)"
msgstr ""
-#: src/trayicon.py:156
+#: NetworkMgr/trayicon.py:349
msgid "Available Connections"
msgstr ""
-#: src/trayicon.py:364
+#: NetworkMgr/trayicon.py:367
+msgid "Forget Network"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:835
+#, python-format
+msgid ""
+"%(card)s did not associate with %(ssid)s before timing out.\n"
+"\n"
+"A weak signal is the usual cause."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:839
+#, python-format
+msgid "wpa_supplicant did not accept the request on %s."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:842
+#, python-format
+msgid "Could not connect to %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:873
msgid "Wi-Fi Network Authentication Required"
msgstr ""
-#: src/trayicon.py:376
-#, python-brace-format
-msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
+#: NetworkMgr/trayicon.py:886
+#, python-format
+msgid "Could not connect to %s. Try again."
msgstr ""
-#: src/trayicon.py:378
-#, python-brace-format
-msgid "Authentication required by {ssid_info[0]} Wi-Fi Network"
+#: NetworkMgr/trayicon.py:888
+#, python-format
+msgid "Authentication required by %s Wi-Fi Network"
msgstr ""
-#: src/trayicon.py:381
+#: NetworkMgr/trayicon.py:890 NetworkMgr/trayicon.py:1086
msgid "Password:"
msgstr ""
-#: src/trayicon.py:384
+#: NetworkMgr/trayicon.py:893
msgid "Show password"
msgstr ""
+
+#: NetworkMgr/trayicon.py:1014
+msgid "Enterprise Wi-Fi Authentication"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1023
+#, python-format
+msgid "%s Enterprise Authentication Failed"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1025
+#, python-format
+msgid "Enterprise Authentication for %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1029
+#, python-format
+msgid "Security: %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1041
+msgid "EAP Method:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1054
+msgid "Inner Auth:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1067
+msgid "Username:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1076
+msgid "Anonymous ID:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1080
+msgid "Optional - for privacy"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1094
+msgid "Show"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1103
+msgid "CA Certificate:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1108
+msgid "Select CA Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1116
+msgid "Certificates (*.pem, *.crt, *.cer)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1128
+msgid "Client Cert:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1132
+msgid "Select Client Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1143
+msgid "Private Key:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1147
+msgid "Select Private Key"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1151
+msgid "Key files (*.pem, *.key, *.p12)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1163
+msgid "Key Password:"
+msgstr ""
diff --git a/po/pt_Br.po b/po/pt_Br.po
index 632763e..80ce6f1 100644
--- a/po/pt_Br.po
+++ b/po/pt_Br.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-08-29 15:54+0800\n"
+"POT-Creation-Date: 2026-09-11 21:33-0300\n"
"PO-Revision-Date: 2025-01-05 16:17-0400\n"
"Last-Translator: Edu_Amr \n"
"Language-Team: Portuguese \n"
@@ -17,97 +17,236 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/trayicon.py:67
+#: NetworkMgr/trayicon.py:173
+msgid "WireGuard VPN"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:182
+#, fuzzy, python-format
+msgid "%s Connected"
+msgstr "Wi-Fi %s Conectado"
+
+#: NetworkMgr/trayicon.py:185 NetworkMgr/trayicon.py:219
+#: NetworkMgr/trayicon.py:230
+#, fuzzy, python-format
+msgid "Disable %s"
+msgstr "Desativar"
+
+#: NetworkMgr/trayicon.py:189
+#, fuzzy, python-format
+msgid "%s Disconnected"
+msgstr "Wi-Fi %s Desconectado"
+
+#: NetworkMgr/trayicon.py:192 NetworkMgr/trayicon.py:212
+msgid "Enable"
+msgstr "Ativar"
+
+#: NetworkMgr/trayicon.py:198
msgid "Ethernet Network"
msgstr "Rede Ethernet"
-#: src/trayicon.py:78
+#: NetworkMgr/trayicon.py:209
+#, fuzzy, python-format
+msgid "Wired %s Disabled"
+msgstr "Wi-Fi %s Desativado"
+
+#: NetworkMgr/trayicon.py:216
#, python-format
msgid "Wired %s Connected"
msgstr "Cabo %s Conectado"
-#: src/trayicon.py:81
-msgid "Disable"
-msgstr "Desativar"
+#: NetworkMgr/trayicon.py:223 NetworkMgr/trayicon.py:234
+#: NetworkMgr/trayicon.py:281
+#, python-format
+msgid "Configure %s"
+msgstr ""
-#: src/trayicon.py:86
+#: NetworkMgr/trayicon.py:227
#, python-format
msgid "Wired %s Disconnected"
msgstr "Cabo %s Desconectado"
-#: src/trayicon.py:89
-msgid "Enable"
-msgstr "Ativar"
-
-#: src/trayicon.py:93
+#: NetworkMgr/trayicon.py:238
#, python-format
msgid "Wired %s Unplug"
msgstr "Cabo %s Desconectado fisicamente"
-#: src/trayicon.py:101
+#: NetworkMgr/trayicon.py:246
#, python-format
msgid "WiFi %s Disabled"
msgstr "Wi-Fi %s Desativado"
-#: src/trayicon.py:104
+#: NetworkMgr/trayicon.py:249
#, python-format
msgid "Enable Wifi %s"
msgstr "Ativar Wi-Fi %s"
-#: src/trayicon.py:109
+#: NetworkMgr/trayicon.py:254
#, python-format
msgid "WiFi %s Disconnected"
msgstr "Wi-Fi %s Desconectado"
-#: src/trayicon.py:113 src/trayicon.py:131
+#: NetworkMgr/trayicon.py:259 NetworkMgr/trayicon.py:278
#, python-format
msgid "Disable Wifi %s"
msgstr "Desativar Wi-Fi %s"
-#: src/trayicon.py:119
+#: NetworkMgr/trayicon.py:265
#, python-format
msgid "WiFi %s Connected"
msgstr "Wi-Fi %s Conectado"
-#: src/trayicon.py:125
+#: NetworkMgr/trayicon.py:271
#, python-format
msgid "Disconnect from %s"
msgstr "Desconectar de %s"
-#: src/trayicon.py:139
-msgid "Enable Networking"
-msgstr "Ativar Rede"
-
-#: src/trayicon.py:143
-msgid "Disable Networking"
+#: NetworkMgr/trayicon.py:287
+#, fuzzy
+msgid "Restart Networking"
msgstr "Desativar Rede"
-#: src/trayicon.py:148
+#: NetworkMgr/trayicon.py:290
msgid "Close Network Manager"
msgstr "Fechar Gerenciador de Rede"
-#: src/trayicon.py:156
+#: NetworkMgr/trayicon.py:312
+#, python-format
+msgid "%s (WPA3 only)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:349
msgid "Available Connections"
msgstr "Conexões Disponíveis"
-#: src/trayicon.py:364
+#: NetworkMgr/trayicon.py:367
+#, fuzzy
+msgid "Forget Network"
+msgstr "Rede Ethernet"
+
+#: NetworkMgr/trayicon.py:835
+#, python-format
+msgid ""
+"%(card)s did not associate with %(ssid)s before timing out.\n"
+"\n"
+"A weak signal is the usual cause."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:839
+#, python-format
+msgid "wpa_supplicant did not accept the request on %s."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:842
+#, python-format
+msgid "Could not connect to %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:873
msgid "Wi-Fi Network Authentication Required"
msgstr "Autenticação Necessária para a Rede Wi-Fi"
-#: src/trayicon.py:376
-#, python-brace-format
-msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
-msgstr "Autenticação falhou na rede Wi-Fi {ssid_info[0]}"
+#: NetworkMgr/trayicon.py:886
+#, python-format
+msgid "Could not connect to %s. Try again."
+msgstr ""
-#: src/trayicon.py:378
-#, python-brace-format
-msgid "Authentication required by {ssid_info[0]} Wi-Fi Network"
+#: NetworkMgr/trayicon.py:888
+#, fuzzy, python-format
+msgid "Authentication required by %s Wi-Fi Network"
msgstr "Autenticação requerida pela rede Wi-Fi {ssid_info[0]}"
-#: src/trayicon.py:381
+#: NetworkMgr/trayicon.py:890 NetworkMgr/trayicon.py:1086
msgid "Password:"
msgstr "Senha:"
-#: src/trayicon.py:384
+#: NetworkMgr/trayicon.py:893
msgid "Show password"
msgstr "Mostrar senha"
+
+#: NetworkMgr/trayicon.py:1014
+msgid "Enterprise Wi-Fi Authentication"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1023
+#, fuzzy, python-format
+msgid "%s Enterprise Authentication Failed"
+msgstr "Autenticação Necessária para a Rede Wi-Fi"
+
+#: NetworkMgr/trayicon.py:1025
+#, python-format
+msgid "Enterprise Authentication for %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1029
+#, python-format
+msgid "Security: %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1041
+msgid "EAP Method:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1054
+msgid "Inner Auth:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1067
+msgid "Username:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1076
+msgid "Anonymous ID:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1080
+msgid "Optional - for privacy"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1094
+msgid "Show"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1103
+msgid "CA Certificate:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1108
+msgid "Select CA Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1116
+msgid "Certificates (*.pem, *.crt, *.cer)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1128
+msgid "Client Cert:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1132
+msgid "Select Client Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1143
+msgid "Private Key:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1147
+msgid "Select Private Key"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1151
+msgid "Key files (*.pem, *.key, *.p12)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1163
+#, fuzzy
+msgid "Key Password:"
+msgstr "Senha:"
+
+#~ msgid "Enable Networking"
+#~ msgstr "Ativar Rede"
+
+#, python-brace-format
+#~ msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
+#~ msgstr "Autenticação falhou na rede Wi-Fi {ssid_info[0]}"
diff --git a/po/ru.po b/po/ru.po
index 1d0ee0e..ef956bc 100644
--- a/po/ru.po
+++ b/po/ru.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-08-29 15:54+0800\n"
+"POT-Creation-Date: 2026-09-11 21:33-0300\n"
"PO-Revision-Date: 2022-08-30 00:05+0300\n"
"Last-Translator: Alexander Alexeev \n"
"Language-Team: Language locale/ru\n"
@@ -17,97 +17,236 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: src/trayicon.py:67
+#: NetworkMgr/trayicon.py:173
+msgid "WireGuard VPN"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:182
+#, fuzzy, python-format
+msgid "%s Connected"
+msgstr "Подключение по WiFi %s установлено"
+
+#: NetworkMgr/trayicon.py:185 NetworkMgr/trayicon.py:219
+#: NetworkMgr/trayicon.py:230
+#, fuzzy, python-format
+msgid "Disable %s"
+msgstr "Отключить"
+
+#: NetworkMgr/trayicon.py:189
+#, fuzzy, python-format
+msgid "%s Disconnected"
+msgstr "Подключение по WiFi %s разорвано"
+
+#: NetworkMgr/trayicon.py:192 NetworkMgr/trayicon.py:212
+msgid "Enable"
+msgstr "Включить"
+
+#: NetworkMgr/trayicon.py:198
msgid "Ethernet Network"
msgstr "Сеть Ethernet"
-#: src/trayicon.py:78
+#: NetworkMgr/trayicon.py:209
+#, fuzzy, python-format
+msgid "Wired %s Disabled"
+msgstr "Соединение по WiFi %s отключено"
+
+#: NetworkMgr/trayicon.py:216
#, python-format
msgid "Wired %s Connected"
msgstr "Проводное соединение %s подключено"
-#: src/trayicon.py:81
-msgid "Disable"
-msgstr "Отключить"
+#: NetworkMgr/trayicon.py:223 NetworkMgr/trayicon.py:234
+#: NetworkMgr/trayicon.py:281
+#, python-format
+msgid "Configure %s"
+msgstr ""
-#: src/trayicon.py:86
+#: NetworkMgr/trayicon.py:227
#, python-format
msgid "Wired %s Disconnected"
msgstr "Проводное соединение %s отключено"
-#: src/trayicon.py:89
-msgid "Enable"
-msgstr "Включить"
-
-#: src/trayicon.py:93
+#: NetworkMgr/trayicon.py:238
#, python-format
msgid "Wired %s Unplug"
msgstr "Проводное соединение %s разорвано"
-#: src/trayicon.py:101
+#: NetworkMgr/trayicon.py:246
#, python-format
msgid "WiFi %s Disabled"
msgstr "Соединение по WiFi %s отключено"
-#: src/trayicon.py:104
+#: NetworkMgr/trayicon.py:249
#, python-format
msgid "Enable Wifi %s"
msgstr "Включить соединение по WiFi %s"
-#: src/trayicon.py:109
+#: NetworkMgr/trayicon.py:254
#, python-format
msgid "WiFi %s Disconnected"
msgstr "Подключение по WiFi %s разорвано"
-#: src/trayicon.py:113 src/trayicon.py:131
+#: NetworkMgr/trayicon.py:259 NetworkMgr/trayicon.py:278
#, python-format
msgid "Disable Wifi %s"
msgstr "Отключить соединение по WiFi %s"
-#: src/trayicon.py:119
+#: NetworkMgr/trayicon.py:265
#, python-format
msgid "WiFi %s Connected"
msgstr "Подключение по WiFi %s установлено"
-#: src/trayicon.py:125
+#: NetworkMgr/trayicon.py:271
#, python-format
msgid "Disconnect from %s"
msgstr "Отключиться от %s"
-#: src/trayicon.py:139
-msgid "Enable Networking"
-msgstr "Включить сеть"
-
-#: src/trayicon.py:143
-msgid "Disable Networking"
+#: NetworkMgr/trayicon.py:287
+#, fuzzy
+msgid "Restart Networking"
msgstr "Отключить сеть"
-#: src/trayicon.py:148
+#: NetworkMgr/trayicon.py:290
msgid "Close Network Manager"
msgstr "Закрыть Network Manager"
-#: src/trayicon.py:156
+#: NetworkMgr/trayicon.py:312
+#, python-format
+msgid "%s (WPA3 only)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:349
msgid "Available Connections"
msgstr "Доступные подключения"
-#: src/trayicon.py:364
+#: NetworkMgr/trayicon.py:367
+#, fuzzy
+msgid "Forget Network"
+msgstr "Сеть Ethernet"
+
+#: NetworkMgr/trayicon.py:835
+#, python-format
+msgid ""
+"%(card)s did not associate with %(ssid)s before timing out.\n"
+"\n"
+"A weak signal is the usual cause."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:839
+#, python-format
+msgid "wpa_supplicant did not accept the request on %s."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:842
+#, python-format
+msgid "Could not connect to %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:873
msgid "Wi-Fi Network Authentication Required"
msgstr "Требуется региcтрация в сети WiFi"
-#: src/trayicon.py:376
-#, python-brace-format
-msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
-msgstr "Ошибка при регистрации в сети WiFi {ssid_info[0]}"
+#: NetworkMgr/trayicon.py:886
+#, python-format
+msgid "Could not connect to %s. Try again."
+msgstr ""
-#: src/trayicon.py:378
-#, python-brace-format
-msgid "Authentication required by {ssid_info[0]} Wi-Fi Network"
+#: NetworkMgr/trayicon.py:888
+#, fuzzy, python-format
+msgid "Authentication required by %s Wi-Fi Network"
msgstr "Требуется региcтрация в сети WiFi {ssid_info[0]}"
-#: src/trayicon.py:381
+#: NetworkMgr/trayicon.py:890 NetworkMgr/trayicon.py:1086
msgid "Password:"
msgstr "Пароль:"
-#: src/trayicon.py:384
+#: NetworkMgr/trayicon.py:893
msgid "Show password"
msgstr "Показать пароль"
+
+#: NetworkMgr/trayicon.py:1014
+msgid "Enterprise Wi-Fi Authentication"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1023
+#, fuzzy, python-format
+msgid "%s Enterprise Authentication Failed"
+msgstr "Требуется региcтрация в сети WiFi"
+
+#: NetworkMgr/trayicon.py:1025
+#, python-format
+msgid "Enterprise Authentication for %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1029
+#, python-format
+msgid "Security: %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1041
+msgid "EAP Method:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1054
+msgid "Inner Auth:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1067
+msgid "Username:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1076
+msgid "Anonymous ID:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1080
+msgid "Optional - for privacy"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1094
+msgid "Show"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1103
+msgid "CA Certificate:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1108
+msgid "Select CA Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1116
+msgid "Certificates (*.pem, *.crt, *.cer)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1128
+msgid "Client Cert:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1132
+msgid "Select Client Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1143
+msgid "Private Key:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1147
+msgid "Select Private Key"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1151
+msgid "Key files (*.pem, *.key, *.p12)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1163
+#, fuzzy
+msgid "Key Password:"
+msgstr "Пароль:"
+
+#~ msgid "Enable Networking"
+#~ msgstr "Включить сеть"
+
+#, python-brace-format
+#~ msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
+#~ msgstr "Ошибка при регистрации в сети WiFi {ssid_info[0]}"
diff --git a/po/sv.po b/po/sv.po
index 2060351..42c08b3 100644
--- a/po/sv.po
+++ b/po/sv.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-12-14 17:25+0000\n"
+"POT-Creation-Date: 2026-09-11 21:33-0300\n"
"PO-Revision-Date: 2023-12-14 17:25+0000\n"
"Last-Translator: Joshua Hoffmann \n"
"Language-Team: Language locale/sv\n"
@@ -17,97 +17,236 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: src/trayicon.py:67
+#: NetworkMgr/trayicon.py:173
+msgid "WireGuard VPN"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:182
+#, fuzzy, python-format
+msgid "%s Connected"
+msgstr "WiFi %s ansluten"
+
+#: NetworkMgr/trayicon.py:185 NetworkMgr/trayicon.py:219
+#: NetworkMgr/trayicon.py:230
+#, fuzzy, python-format
+msgid "Disable %s"
+msgstr "Inaktivera"
+
+#: NetworkMgr/trayicon.py:189
+#, fuzzy, python-format
+msgid "%s Disconnected"
+msgstr "WiFi %s har kopplats bort"
+
+#: NetworkMgr/trayicon.py:192 NetworkMgr/trayicon.py:212
+msgid "Enable"
+msgstr "Aktivera"
+
+#: NetworkMgr/trayicon.py:198
msgid "Ethernet Network"
msgstr "Ethernet-nätverk"
-#: src/trayicon.py:78
+#: NetworkMgr/trayicon.py:209
+#, fuzzy, python-format
+msgid "Wired %s Disabled"
+msgstr "WiFi %s inaktiverat"
+
+#: NetworkMgr/trayicon.py:216
#, python-format
msgid "Wired %s Connected"
msgstr "trådbunden anslutning %s ansluten"
-#: src/trayicon.py:81
-msgid "Disable"
-msgstr "Inaktivera"
+#: NetworkMgr/trayicon.py:223 NetworkMgr/trayicon.py:234
+#: NetworkMgr/trayicon.py:281
+#, python-format
+msgid "Configure %s"
+msgstr ""
-#: src/trayicon.py:86
+#: NetworkMgr/trayicon.py:227
#, python-format
msgid "Wired %s Disconnected"
msgstr "Kabel frånkopplad från %s"
-#: src/trayicon.py:89
-msgid "Enable"
-msgstr "Aktivera"
-
-#: src/trayicon.py:93
+#: NetworkMgr/trayicon.py:238
#, python-format
msgid "Wired %s Unplug"
msgstr "Kabel borttagen från %s"
-#: src/trayicon.py:101
+#: NetworkMgr/trayicon.py:246
#, python-format
msgid "WiFi %s Disabled"
msgstr "WiFi %s inaktiverat"
-#: src/trayicon.py:104
+#: NetworkMgr/trayicon.py:249
#, python-format
msgid "Enable Wifi %s"
msgstr "Aktivera WiFi %s"
-#: src/trayicon.py:109
+#: NetworkMgr/trayicon.py:254
#, python-format
msgid "WiFi %s Disconnected"
msgstr "WiFi %s har kopplats bort"
-#: src/trayicon.py:113 src/trayicon.py:131
+#: NetworkMgr/trayicon.py:259 NetworkMgr/trayicon.py:278
#, python-format
msgid "Disable Wifi %s"
msgstr "Inaktivera WiFi %s"
-#: src/trayicon.py:119
+#: NetworkMgr/trayicon.py:265
#, python-format
msgid "WiFi %s Connected"
msgstr "WiFi %s ansluten"
-#: src/trayicon.py:125
+#: NetworkMgr/trayicon.py:271
#, python-format
msgid "Disconnect from %s"
msgstr "Koppla bort %s"
-#: src/trayicon.py:139
-msgid "Enable Networking"
-msgstr "Aktivera nätverk"
-
-#: src/trayicon.py:143
-msgid "Disable Networking"
+#: NetworkMgr/trayicon.py:287
+#, fuzzy
+msgid "Restart Networking"
msgstr "Avaktivera nätverksanslutningar"
-#: src/trayicon.py:148
+#: NetworkMgr/trayicon.py:290
msgid "Close Network Manager"
msgstr "Stäng Network Manager"
-#: src/trayicon.py:156
+#: NetworkMgr/trayicon.py:312
+#, python-format
+msgid "%s (WPA3 only)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:349
msgid "Available Connections"
msgstr "Tillgängliga anslutningar"
-#: src/trayicon.py:364
+#: NetworkMgr/trayicon.py:367
+#, fuzzy
+msgid "Forget Network"
+msgstr "Ethernet-nätverk"
+
+#: NetworkMgr/trayicon.py:835
+#, python-format
+msgid ""
+"%(card)s did not associate with %(ssid)s before timing out.\n"
+"\n"
+"A weak signal is the usual cause."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:839
+#, python-format
+msgid "wpa_supplicant did not accept the request on %s."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:842
+#, python-format
+msgid "Could not connect to %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:873
msgid "Wi-Fi Network Authentication Required"
msgstr "Wi-Fi-nätverksautentisering krävs"
-#: src/trayicon.py:376
-#, python-brace-format
-msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
-msgstr "Wi-Fi-nätverksautentisering för {ssid_info[0]} misslyckades"
+#: NetworkMgr/trayicon.py:886
+#, python-format
+msgid "Could not connect to %s. Try again."
+msgstr ""
-#: src/trayicon.py:378
-#, python-brace-format
-msgid "Authentication required by {ssid_info[0]} Wi-Fi Network"
+#: NetworkMgr/trayicon.py:888
+#, fuzzy, python-format
+msgid "Authentication required by %s Wi-Fi Network"
msgstr "Autentisering krävs av Wi-Fi-nätverk {ssid_info[0]}"
-#: src/trayicon.py:381
+#: NetworkMgr/trayicon.py:890 NetworkMgr/trayicon.py:1086
msgid "Password:"
msgstr "Lösenord:"
-#: src/trayicon.py:384
+#: NetworkMgr/trayicon.py:893
msgid "Show password"
msgstr "Visa lösenord"
+
+#: NetworkMgr/trayicon.py:1014
+msgid "Enterprise Wi-Fi Authentication"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1023
+#, fuzzy, python-format
+msgid "%s Enterprise Authentication Failed"
+msgstr "Wi-Fi-nätverksautentisering krävs"
+
+#: NetworkMgr/trayicon.py:1025
+#, python-format
+msgid "Enterprise Authentication for %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1029
+#, python-format
+msgid "Security: %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1041
+msgid "EAP Method:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1054
+msgid "Inner Auth:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1067
+msgid "Username:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1076
+msgid "Anonymous ID:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1080
+msgid "Optional - for privacy"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1094
+msgid "Show"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1103
+msgid "CA Certificate:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1108
+msgid "Select CA Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1116
+msgid "Certificates (*.pem, *.crt, *.cer)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1128
+msgid "Client Cert:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1132
+msgid "Select Client Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1143
+msgid "Private Key:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1147
+msgid "Select Private Key"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1151
+msgid "Key files (*.pem, *.key, *.p12)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1163
+#, fuzzy
+msgid "Key Password:"
+msgstr "Lösenord:"
+
+#~ msgid "Enable Networking"
+#~ msgstr "Aktivera nätverk"
+
+#, python-brace-format
+#~ msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
+#~ msgstr "Wi-Fi-nätverksautentisering för {ssid_info[0]} misslyckades"
diff --git a/po/zh_CN.po b/po/zh_CN.po
index e0fb0b3..cdd871c 100644
--- a/po/zh_CN.po
+++ b/po/zh_CN.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: networkmgr\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-08-29 15:54+0800\n"
+"POT-Creation-Date: 2026-09-11 21:33-0300\n"
"PO-Revision-Date: 2025-09-07 10:33+0800\n"
"Last-Translator: ykla \n"
"Language-Team: ykla \n"
@@ -18,97 +18,236 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.7\n"
-#: src/trayicon.py:67
+#: NetworkMgr/trayicon.py:173
+msgid "WireGuard VPN"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:182
+#, fuzzy, python-format
+msgid "%s Connected"
+msgstr "WiFi %s 已连接"
+
+#: NetworkMgr/trayicon.py:185 NetworkMgr/trayicon.py:219
+#: NetworkMgr/trayicon.py:230
+#, fuzzy, python-format
+msgid "Disable %s"
+msgstr "禁用"
+
+#: NetworkMgr/trayicon.py:189
+#, fuzzy, python-format
+msgid "%s Disconnected"
+msgstr "WiFi %s 已断开"
+
+#: NetworkMgr/trayicon.py:192 NetworkMgr/trayicon.py:212
+msgid "Enable"
+msgstr "启用"
+
+#: NetworkMgr/trayicon.py:198
msgid "Ethernet Network"
msgstr "以太网"
-#: src/trayicon.py:78
+#: NetworkMgr/trayicon.py:209
+#, fuzzy, python-format
+msgid "Wired %s Disabled"
+msgstr "WiFi %s 已禁用"
+
+#: NetworkMgr/trayicon.py:216
#, python-format
msgid "Wired %s Connected"
msgstr "有线 %s 已连接"
-#: src/trayicon.py:81
-msgid "Disable"
-msgstr "禁用"
+#: NetworkMgr/trayicon.py:223 NetworkMgr/trayicon.py:234
+#: NetworkMgr/trayicon.py:281
+#, python-format
+msgid "Configure %s"
+msgstr ""
-#: src/trayicon.py:86
+#: NetworkMgr/trayicon.py:227
#, python-format
msgid "Wired %s Disconnected"
msgstr "有线 %s 已断开"
-#: src/trayicon.py:89
-msgid "Enable"
-msgstr "启用"
-
-#: src/trayicon.py:93
+#: NetworkMgr/trayicon.py:238
#, python-format
msgid "Wired %s Unplug"
msgstr "已拔出有线 %s"
-#: src/trayicon.py:101
+#: NetworkMgr/trayicon.py:246
#, python-format
msgid "WiFi %s Disabled"
msgstr "WiFi %s 已禁用"
-#: src/trayicon.py:104
+#: NetworkMgr/trayicon.py:249
#, python-format
msgid "Enable Wifi %s"
msgstr "启用 WiFi %s"
-#: src/trayicon.py:109
+#: NetworkMgr/trayicon.py:254
#, python-format
msgid "WiFi %s Disconnected"
msgstr "WiFi %s 已断开"
-#: src/trayicon.py:113 src/trayicon.py:131
+#: NetworkMgr/trayicon.py:259 NetworkMgr/trayicon.py:278
#, python-format
msgid "Disable Wifi %s"
msgstr "禁用 WiFi %s"
-#: src/trayicon.py:119
+#: NetworkMgr/trayicon.py:265
#, python-format
msgid "WiFi %s Connected"
msgstr "WiFi %s 已连接"
-#: src/trayicon.py:125
+#: NetworkMgr/trayicon.py:271
#, python-format
msgid "Disconnect from %s"
msgstr "已与 %s 断开连接"
-#: src/trayicon.py:139
-msgid "Enable Networking"
-msgstr "启用网络"
-
-#: src/trayicon.py:143
-msgid "Disable Networking"
+#: NetworkMgr/trayicon.py:287
+#, fuzzy
+msgid "Restart Networking"
msgstr "禁用网络"
-#: src/trayicon.py:148
+#: NetworkMgr/trayicon.py:290
msgid "Close Network Manager"
msgstr "退出网络管理器"
-#: src/trayicon.py:156
+#: NetworkMgr/trayicon.py:312
+#, python-format
+msgid "%s (WPA3 only)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:349
msgid "Available Connections"
msgstr "可用连接"
-#: src/trayicon.py:364
+#: NetworkMgr/trayicon.py:367
+#, fuzzy
+msgid "Forget Network"
+msgstr "以太网"
+
+#: NetworkMgr/trayicon.py:835
+#, python-format
+msgid ""
+"%(card)s did not associate with %(ssid)s before timing out.\n"
+"\n"
+"A weak signal is the usual cause."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:839
+#, python-format
+msgid "wpa_supplicant did not accept the request on %s."
+msgstr ""
+
+#: NetworkMgr/trayicon.py:842
+#, python-format
+msgid "Could not connect to %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:873
msgid "Wi-Fi Network Authentication Required"
msgstr "需要无线网络身份验证"
-#: src/trayicon.py:376
-#, python-brace-format
-msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
-msgstr "无线网络 {ssid_info[0]} 身份验证失败"
+#: NetworkMgr/trayicon.py:886
+#, python-format
+msgid "Could not connect to %s. Try again."
+msgstr ""
-#: src/trayicon.py:378
-#, python-brace-format
-msgid "Authentication required by {ssid_info[0]} Wi-Fi Network"
+#: NetworkMgr/trayicon.py:888
+#, fuzzy, python-format
+msgid "Authentication required by %s Wi-Fi Network"
msgstr "无线网络 {ssid_info[0]} 需要身份验证"
-#: src/trayicon.py:381
+#: NetworkMgr/trayicon.py:890 NetworkMgr/trayicon.py:1086
msgid "Password:"
msgstr "密码:"
-#: src/trayicon.py:384
+#: NetworkMgr/trayicon.py:893
msgid "Show password"
msgstr "显示密码"
+
+#: NetworkMgr/trayicon.py:1014
+msgid "Enterprise Wi-Fi Authentication"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1023
+#, fuzzy, python-format
+msgid "%s Enterprise Authentication Failed"
+msgstr "需要无线网络身份验证"
+
+#: NetworkMgr/trayicon.py:1025
+#, python-format
+msgid "Enterprise Authentication for %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1029
+#, python-format
+msgid "Security: %s"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1041
+msgid "EAP Method:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1054
+msgid "Inner Auth:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1067
+msgid "Username:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1076
+msgid "Anonymous ID:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1080
+msgid "Optional - for privacy"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1094
+msgid "Show"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1103
+msgid "CA Certificate:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1108
+msgid "Select CA Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1116
+msgid "Certificates (*.pem, *.crt, *.cer)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1128
+msgid "Client Cert:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1132
+msgid "Select Client Certificate"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1143
+msgid "Private Key:"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1147
+msgid "Select Private Key"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1151
+msgid "Key files (*.pem, *.key, *.p12)"
+msgstr ""
+
+#: NetworkMgr/trayicon.py:1163
+#, fuzzy
+msgid "Key Password:"
+msgstr "密码:"
+
+#~ msgid "Enable Networking"
+#~ msgstr "启用网络"
+
+#, python-brace-format
+#~ msgid "{ssid_info[0]} Wi-Fi Network Authentication failed"
+#~ msgstr "无线网络 {ssid_info[0]} 身份验证失败"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 02a3efe..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-attrs==21.2.0
-coverage==6.2
-iniconfig==1.1.1
-packaging==21.3
-pluggy==1.0.0
-py==1.11.0
-pyparsing==3.0.6
-pytest==9.0.3
-pytest-cov==3.0.0
-Tkinter==0.0.0
-toml==0.10.2
-tomli==2.0.0
diff --git a/setup.py b/setup.py
index 79afc46..57acea8 100755
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+"""Distutils/setuptools packaging for NetworkMgr.
+
+Installs the NetworkMgr package, the two entry scripts, the devd action
+scripts, the devd and sudoers fragments, the icons and the translations.
+"""
+
import os
import sys
from platform import system
@@ -10,19 +16,27 @@
from DistUtilsExtra.command.build_i18n import build_i18n
from DistUtilsExtra.command.clean_i18n import clean_i18n
-__VERSION__ = '6.9'
+__VERSION__ = '7.0'
PROGRAM_VERSION = __VERSION__
-prefix = '/usr/local' if system() == 'FreeBSD' else sys.prefix
+PREFIX = '/usr/local' if system() == 'FreeBSD' else sys.prefix
def datafilelist(installbase, sourcebase):
- datafileList = []
- for root, subFolders, files in os.walk(sourcebase):
- fileList = []
- for f in files:
- fileList.append(os.path.join(root, f))
- datafileList.append((root.replace(sourcebase, installbase), fileList))
- return datafileList
+ """Map a source directory tree onto its install destination.
+
+ Args:
+ installbase (str): Directory the tree is installed under.
+ sourcebase (str): Directory the tree is read from.
+
+ Returns:
+ list[tuple[str, list[str]]]: A (destination, files) pair per directory,
+ in the form setup()'s data_files expects.
+ """
+ data_file_list = []
+ for root, _, files in os.walk(sourcebase):
+ file_list = [os.path.join(root, name) for name in files]
+ data_file_list.append((root.replace(sourcebase, installbase), file_list))
+ return data_file_list
class UpdateTranslationsCommand(Command):
"""Custom command to extract messages and update .po files."""
@@ -31,18 +45,19 @@ class UpdateTranslationsCommand(Command):
user_options = [] # No custom options
def initialize_options(self):
- pass
+ """Set up the command's options. This command takes none."""
def finalize_options(self):
- pass
+ """Validate the command's options. This command takes none."""
def run(self):
+ """Extract messages into the .pot file and merge them into every .po."""
# Define paths
pot_file = 'po/networkmgr.pot'
po_files = glob.glob('po/*.po')
# Step 1: Extract messages to .pot file
print("Extracting messages to .pot file...")
- os.system(f'xgettext --from-code=UTF-8 -L Python -o {pot_file} networkmgr/*.py networkmgr')
+ os.system(f'xgettext --from-code=UTF-8 -L Python -o {pot_file} NetworkMgr/*.py networkmgr')
# Step 2: Update .po files with the new .pot file
print("Updating .po files with new translations...")
for po_file in po_files:
@@ -59,13 +74,20 @@ class CreateTranslationCommand(Command):
]
def initialize_options(self):
- self.locale = None # Initialize the locale option to None
+ """Set the locale option to its unset default."""
+ self.locale = None
def finalize_options(self):
+ """Check that a locale was given.
+
+ Raises:
+ ValueError: If --locale was not passed on the command line.
+ """
if self.locale is None:
- raise Exception("You must specify the locale code (e.g., --locale=fr)")
+ raise ValueError("You must specify the locale code (e.g., --locale=fr)")
def run(self):
+ """Create a .po file for the requested locale, extracting .pot first."""
# Define paths
pot_file = 'po/networkmgr.pot'
po_dir = 'po'
@@ -73,7 +95,7 @@ def run(self):
# Check if the .pot file exists
if not os.path.exists(pot_file):
print("Extracting messages to .pot file...")
- os.system(f'xgettext --from-code=UTF-8 -L Python -o {pot_file} networkmgr/*.py networkmgr')
+ os.system(f'xgettext --from-code=UTF-8 -L Python -o {pot_file} NetworkMgr/*.py networkmgr')
# Create the new .po file
if not os.path.exists(po_file):
print(f"Creating new {po_file} for locale '{self.locale}'...")
@@ -86,18 +108,19 @@ def run(self):
networkmgr_share = [
'src/auto-switch.py',
'src/link-up.py',
- 'src/setup-nic.py'
+ 'src/setup-nic.py',
+ 'src/wpa_supplicant.conf'
]
data_files = [
- (f'{prefix}/etc/xdg/autostart', ['src/networkmgr.desktop']),
- (f'{prefix}/share/networkmgr', networkmgr_share),
- (f'{prefix}/etc/sudoers.d', ['src/sudoers.d/networkmgr']),
- (f'{prefix}/etc/devd', ['src/networkmgr.conf'])
+ (f'{PREFIX}/etc/xdg/autostart', ['src/networkmgr.desktop']),
+ (f'{PREFIX}/share/networkmgr', networkmgr_share),
+ (f'{PREFIX}/etc/sudoers.d', ['src/sudoers.d/networkmgr']),
+ (f'{PREFIX}/etc/devd', ['src/networkmgr.conf'])
]
-data_files.extend(datafilelist(f'{prefix}/share/icons/hicolor', 'src/icons'))
-data_files.extend(datafilelist(f'{prefix}/share/locale', 'build/mo'))
+data_files.extend(datafilelist(f'{PREFIX}/share/icons/hicolor', 'src/icons'))
+data_files.extend(datafilelist(f'{PREFIX}/share/locale', 'build/mo'))
setup(
@@ -121,4 +144,4 @@ def run(self):
}
)
-run('gtk-update-icon-cache -f /usr/local/share/icons/hicolor', shell=True)
+run('gtk-update-icon-cache -f /usr/local/share/icons/hicolor', shell=True, check=False)
diff --git a/src/auto-switch.py b/src/auto-switch.py
index cc0ad04..1e2246e 100755
--- a/src/auto-switch.py
+++ b/src/auto-switch.py
@@ -9,46 +9,53 @@
args = sys.argv
if len(args) != 2:
- exit()
+ sys.exit()
nic = args[1]
-not_nics_regex = r"(enc|lo|fwe|fwip|tap|plip|pfsync|pflog|ipfw|tun|sl|faith|" \
+NOT_NICS_REGEX = r"(enc|lo|fwe|fwip|tap|plip|pfsync|pflog|ipfw|tun|sl|faith|" \
r"ppp|bridge|wg)[0-9]|vm-[a-z]"
-default_nic = run(
- 'netstat -rn | grep default',
- stdout=PIPE,
- shell=True,
- universal_newlines=True
-).stdout
+# The default route lines, picked out here rather than through a shell pipe.
+DEFAULT_NIC = "\n".join(
+ line for line in run(
+ ['netstat', '-rn'],
+ stdout=PIPE,
+ universal_newlines=True,
+ check=False
+ ).stdout.splitlines()
+ if 'default' in line
+)
nics = run(
['ifconfig', '-l', 'ether'],
stdout=PIPE,
- universal_newlines=True
+ universal_newlines=True,
+ check=False
)
nics_left_over = nics.stdout.replace(nic, '').strip()
-nic_list = sorted(re.sub(not_nics_regex, '', nics_left_over).strip().split())
+nic_list = sorted(re.sub(NOT_NICS_REGEX, '', nics_left_over).strip().split())
# Stop the script if the nic is not valid or not in the default route.
-if re.search(not_nics_regex, nic):
- exit(0)
-elif nic not in default_nic:
- exit(0)
+if re.search(NOT_NICS_REGEX, nic):
+ sys.exit(0)
+elif nic not in DEFAULT_NIC:
+ sys.exit(0)
elif not nic_list:
- exit(0)
+ sys.exit(0)
nic_ifconfig = run(
['ifconfig', nic],
stdout=PIPE,
- universal_newlines=True
+ universal_newlines=True,
+ check=False
).stdout
dhcp = run(
['sysrc', '-n', f'ifconfig_{nic}'],
stdout=PIPE,
- universal_newlines=True
+ universal_newlines=True,
+ check=False
).stdout
active_status = (
@@ -61,16 +68,18 @@
# Restarting routing adds and nic if there is another one that is active
# or associated.
if not any(active_status):
- run(['service', 'netif', 'stop', nic])
- # Create a marker file for link-up.py to detect runtime state change vs boot
- with open(f'/tmp/link-down-{nic}', 'w') as f:
+ run(['service', 'netif', 'stop', nic], check=False)
+ # Create a marker file for link-up.py to detect runtime state change vs boot.
+ # /var/run: root-only, and cleanvar empties it at every boot.
+ with open(f'/var/run/link-down-{nic}', 'w', encoding='utf-8') as f:
f.write('down')
if dhcp.strip() == 'DHCP':
for current_nic in nic_list:
output = run(
['ifconfig', current_nic],
stdout=PIPE,
- universal_newlines=True
+ universal_newlines=True,
+ check=False
)
nic_ifconfig = output.stdout
status_types = [
@@ -80,7 +89,7 @@
found_status = re.search(f"status: ({'|'.join(status_types)})", nic_ifconfig)
found_inet = re.search(r"inet(\s|6)", nic_ifconfig)
if found_status and found_inet:
- run(['service', 'dhclient', 'restart', current_nic])
+ run(['service', 'dhclient', 'restart', current_nic], check=False)
break
else:
- run(['service', 'routing', 'restart'])
+ run(['service', 'routing', 'restart'], check=False)
diff --git a/src/link-up.py b/src/link-up.py
index d1021f8..6da7831 100755
--- a/src/link-up.py
+++ b/src/link-up.py
@@ -1,5 +1,12 @@
#!/usr/local/bin/python3
+"""devd action for IFNET LINK_UP events on ethernet interfaces.
+
+Invoked by /usr/local/etc/devd/networkmgr.conf with the interface name as its
+only argument. Restores DHCP on an interface that auto-switch.py previously
+marked as down, or otherwise starts dhclient quietly.
+"""
+
import os
import re
import sys
@@ -7,30 +14,31 @@
args = sys.argv
if len(args) != 2:
- exit(1)
+ sys.exit(1)
nic = args[1]
-not_nics_regex = r"(enc|lo|fwe|fwip|tap|plip|pfsync|pflog|ipfw|tun|sl|faith|" \
+NOT_NICS_REGEX = r"(enc|lo|fwe|fwip|tap|plip|pfsync|pflog|ipfw|tun|sl|faith|" \
r"ppp|bridge|wg|wlan)[0-9]+|vm-[a-z]+"
# Stop the script if the nic is not valid.
-if re.search(not_nics_regex, nic):
- exit(0)
+if re.search(NOT_NICS_REGEX, nic):
+ sys.exit(0)
# This marker file is created by auto-switch.py when the nic is down.
-if os.path.exists(f'/tmp/link-down-{nic}'):
+if os.path.exists(f'/var/run/link-down-{nic}'):
nic_ifconfig = run(
['ifconfig', nic],
stdout=PIPE,
- universal_newlines=True
+ universal_newlines=True,
+ check=False
).stdout
if 'inet ' not in nic_ifconfig:
- run(['service', 'netif', 'start', nic])
+ run(['service', 'netif', 'start', nic], check=False)
- run(['service', 'routing', 'restart'])
- run(['service', 'dhclient', 'restart', nic])
+ run(['service', 'routing', 'restart'], check=False)
+ run(['service', 'dhclient', 'restart', nic], check=False)
# Clean up marker file
- os.remove(f'/tmp/link-down-{nic}')
+ os.remove(f'/var/run/link-down-{nic}')
else:
- run(['service', 'dhclient', 'quietstart', nic])
+ run(['service', 'dhclient', 'quietstart', nic], check=False)
diff --git a/src/setup-nic.py b/src/setup-nic.py
index ee57618..2d6fdd1 100755
--- a/src/setup-nic.py
+++ b/src/setup-nic.py
@@ -1,5 +1,15 @@
#!/usr/local/bin/python3
+"""devd action for IFNET ATTACH events and wireless driver attachments.
+
+Invoked by /usr/local/etc/devd/networkmgr.conf with the interface or device
+name as its only argument. Declares the interface in rc.conf if it is not
+declared yet, installs or tops up /etc/wpa_supplicant.conf for a wireless
+device, and brings the interface up through /etc/pccard_ether. Wireless
+children are then marked up with ifconfig, since wpa_supplicant leaves them
+down when it has no network to join.
+"""
+
import os
import re
import shutil
@@ -9,6 +19,14 @@
def file_content(paths):
+ """Read several files and return their contents joined together.
+
+ Args:
+ paths (list[pathlib.Path]): Files to read, in order.
+
+ Returns:
+ str: The concatenated contents of every path.
+ """
buffers = []
for path in paths:
with path.open('r') as file:
@@ -16,46 +34,86 @@ def file_content(paths):
return "".join(buffers)
+def setting_lines(text):
+ """Yield the key and the raw line for every key=value line.
+
+ Blank lines and comments are skipped.
+
+ Args:
+ text (str): Contents of a wpa_supplicant.conf style file.
+
+ Yields:
+ tuple[str, str]: The setting name and the line it came from.
+ """
+ for line in text.splitlines():
+ stripped = line.strip()
+ if not stripped or stripped.startswith('#') or '=' not in stripped:
+ continue
+ yield stripped.split('=', 1)[0].strip(), line
+
+
+def add_missing_settings(conf, template):
+ """Prepend the template globals that a config file does not declare yet.
+
+ Args:
+ conf (pathlib.Path): Config file to update in place.
+ template (pathlib.Path): Template holding the expected globals.
+ """
+ current = conf.read_text()
+ present = {key for key, _ in setting_lines(current)}
+ missing = [line for key, line in setting_lines(template.read_text())
+ if key not in present]
+ if missing:
+ conf.write_text("\n".join(missing) + "\n" + current)
+
+
args = sys.argv
if len(args) != 2:
- exit(1)
+ sys.exit(1)
nic = args[1]
etc = Path(os.sep, "etc")
rc_conf = etc / "rc.conf"
rc_conf_local = etc / "rc.conf.local"
wpa_supplicant = etc / "wpa_supplicant.conf"
+wpa_supplicant_template = Path("/usr/local/share/networkmgr/wpa_supplicant.conf")
rc_conf_paths = [rc_conf]
if rc_conf_local.exists():
rc_conf_paths.append(rc_conf_local)
-rc_conf_content = file_content(rc_conf_paths)
+RC_CONF_CONTENT = file_content(rc_conf_paths)
-not_nics_regex = r"(enc|lo|fwe|fwip|tap|plip|pfsync|pflog|ipfw|tun|sl|faith|" \
+NOT_NICS_REGEX = r"(enc|lo|fwe|fwip|tap|plip|pfsync|pflog|ipfw|tun|sl|faith|" \
r"ppp|bridge|wg|wlan)[0-9]+|vm-[a-z]+"
-# wifi_driver_regex is taken from devd.conf wifi-driver-regex
-wifi_driver_regex = "(ath|ath[0-9]+k|bwi|bwn|ipw|iwlwifi|iwi|iwm|iwn|malo|mwl|mt79|otus|" \
+# WIFI_DRIVER_REGEX is taken from devd.conf wifi-driver-regex
+WIFI_DRIVER_REGEX = "(ath|ath[0-9]+k|bwi|bwn|ipw|iwlwifi|iwi|iwm|iwn|malo|mwl|mt79|otus|" \
"ral|rsu|rtw|rtwn|rum|run|uath|upgt|ural|urtw|wpi|wtap|zyd)[0-9]+"
-if re.search(not_nics_regex, nic):
- exit(0)
+if re.search(NOT_NICS_REGEX, nic):
+ sys.exit(0)
-if re.search(wifi_driver_regex, nic):
+if re.search(WIFI_DRIVER_REGEX, nic):
if not wpa_supplicant.exists():
- wpa_supplicant.touch()
+ shutil.copyfile(wpa_supplicant_template, wpa_supplicant)
shutil.chown(wpa_supplicant, user="root", group="wheel")
wpa_supplicant.chmod(0o600) # Secure: root-only, contains passwords
- if f'wlans_{nic}=' not in rc_conf_content:
- for wlanNum in range(9):
- if f'wlan{wlanNum}' not in rc_conf_content:
- run(['sysrc', f'wlans_{nic}=wlan{wlanNum}'])
- run(['sysrc', f'ifconfig_wlan{wlanNum}=WPA DHCP'])
+ else:
+ add_missing_settings(wpa_supplicant, wpa_supplicant_template)
+ if f'wlans_{nic}=' not in RC_CONF_CONTENT:
+ for wlan_number in range(9):
+ if f'wlan{wlan_number}' not in RC_CONF_CONTENT:
+ run(['sysrc', f'wlans_{nic}=wlan{wlan_number}'], check=False)
+ run(['sysrc', f'ifconfig_wlan{wlan_number}=WPA DHCP'], check=False)
break
- run(['/etc/pccard_ether', nic, 'startchildren'])
+ run(['/etc/pccard_ether', nic, 'startchildren'], check=False)
+ # On the first setup wlan is down
+ run(['ifconfig', nic, 'up'], check=False)
+ else:
+ run(['/etc/pccard_ether', nic, 'startchildren'], check=False)
else:
- if f'ifconfig_{nic}=' not in rc_conf_content:
- run(['sysrc', f'ifconfig_{nic}=DHCP'])
- run(['/etc/pccard_ether', nic, 'start'])
+ if f'ifconfig_{nic}=' not in RC_CONF_CONTENT:
+ run(['sysrc', f'ifconfig_{nic}=DHCP'], check=False)
+ run(['/etc/pccard_ether', nic, 'start'], check=False)
diff --git a/src/sudoers.d/networkmgr b/src/sudoers.d/networkmgr
index 3d9bf4c..63383d6 100644
--- a/src/sudoers.d/networkmgr
+++ b/src/sudoers.d/networkmgr
@@ -1 +1 @@
-%wheel ALL=(ALL) NOPASSWD: /usr/local/bin/networkmgr
+%operator ALL=(ALL) NOPASSWD: /usr/local/bin/networkmgr
diff --git a/src/wpa_supplicant.conf b/src/wpa_supplicant.conf
new file mode 100644
index 0000000..53badc2
--- /dev/null
+++ b/src/wpa_supplicant.conf
@@ -0,0 +1,7 @@
+# Global settings installed by NetworkMgr. Network blocks are appended below
+# by NetworkMgr; setup-nic.py adds any of these lines that are missing.
+ctrl_interface=/var/run/wpa_supplicant
+ctrl_interface_group=operator
+update_config=1
+pmf=1
+autoscan=exponential:3:300
\ No newline at end of file
diff --git a/tests/README.md b/tests/README.md
deleted file mode 100644
index 754b387..0000000
--- a/tests/README.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# Testing networkmgr modules
-
-Testing networkmgr modules with pytest
-
-1. Create a virtual environment using python 3.8
-2. Activate the venv
-3. Install pytest and pytest coverage
-4. Execute pip list to verify your environment.
-
-```bash
-python3.8 -m venv venv38
-
-source venv38/bin/activate
-
-pip install pytest pytest-cov
-
-networkmgr-fork/tests on unit_tests [!?] via 🐍 v3.8.12 (venv38)
-❯ pip list
-Package Version
----------- -------
-attrs 21.2.0
-coverage 6.2
-iniconfig 1.1.1
-packaging 21.3
-pip 21.3.1
-pluggy 1.0.0
-py 1.11.0
-pyparsing 3.0.6
-pytest 6.2.5
-pytest-cov 3.0.0
-setuptools 60.1.0
-sqlite3 0.0.0
-Tkinter 0.0.0
-toml 0.10.2
-tomli 2.0.0
-```
-
-To run the tests navigate to the tests directory and enter the following command,
-
-```bash
-pytest -lv --cov src unit/
-```
-
-The result will be similar to this,
-
-```bash
-❯ pytest -lv --cov src unit/
-================================================== test session starts ===================================================
-platform freebsd13 -- Python 3.8.12, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /usr/home//venv38/bin/python3.8
-cachedir: .pytest_cache
-rootdir: /usr/home/rgeorgia/PycharmProjects/networkmgr-fork
-plugins: cov-3.0.0
-collected 7 items
-
-unit/test_net_api.py::test_default_card_returns_str PASSED [ 14%]
-unit/test_net_api.py::test_card_online PASSED [ 28%]
-unit/test_net_api.py::test_card_not_online PASSED [ 42%]
-unit/test_net_api.py::test_connection_status_card_is_none PASSED [ 57%]
-unit/test_net_api.py::test_connection_status_card_is_default PASSED [ 71%]
-unit/test_net_api.py::test_connection_status_card_is_wlan_not_connected PASSED [ 85%]
-unit/test_net_api.py::test_connection_status_card_is_wlan_connected PASSED [100%]
-
--------- coverage: platform freebsd13, python 3.8.12-final-0 ---------
-Name Stmts Miss Cover
----------------------------------------------------------------------------------------
-/usr/home//networkmgr-fork/src/net_api.py 199 119 40%
----------------------------------------------------------------------------------------
-TOTAL 199 119 40%
-
-
-=================================================== 7 passed in 0.11s ====================================================
-
-
-```
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/unit/test_enterprise_wpa.py b/tests/unit/test_enterprise_wpa.py
deleted file mode 100644
index acf4716..0000000
--- a/tests/unit/test_enterprise_wpa.py
+++ /dev/null
@@ -1,227 +0,0 @@
-"""
-Unit tests for Enterprise WPA (802.1X/EAP) functionality.
-"""
-import sys
-import os
-import tempfile
-from pathlib import Path
-
-import pytest
-
-# Add project root to path
-top_dir = str(Path(__file__).absolute().parent.parent.parent)
-sys.path.insert(0, top_dir)
-
-from NetworkMgr.net_api import (
- EAP_METHODS,
- PHASE2_METHODS,
- DEFAULT_CA_CERT,
- is_enterprise_network,
- get_security_type,
- validate_certificate,
- get_system_ca_certificates,
- generate_eap_config,
-)
-
-
-class TestEnterpriseDetection:
- """Tests for enterprise network detection."""
-
- def test_is_enterprise_network_with_eap(self):
- """Test detection of EAP indicator."""
- assert is_enterprise_network("RSN-EAP WPA2-EAP") is True
- assert is_enterprise_network("WPA2-EAP") is True
- assert is_enterprise_network("EAP") is True
-
- def test_is_enterprise_network_with_8021x(self):
- """Test detection of 802.1X indicator."""
- assert is_enterprise_network("RSN 802.1X") is True
- assert is_enterprise_network("802.1X WPA2") is True
-
- def test_is_enterprise_network_psk(self):
- """Test that PSK networks are not detected as enterprise."""
- assert is_enterprise_network("RSN WPA2-PSK") is False
- assert is_enterprise_network("WPA-PSK") is False
- assert is_enterprise_network("RSN HTCAP WME") is False
-
- def test_is_enterprise_network_open(self):
- """Test that open networks are not detected as enterprise."""
- assert is_enterprise_network("") is False
- assert is_enterprise_network("ESS") is False
-
- def test_is_enterprise_network_case_insensitive(self):
- """Test case-insensitive detection."""
- assert is_enterprise_network("wpa2-eap") is True
- assert is_enterprise_network("Eap") is True
-
-
-class TestSecurityTypeDetection:
- """Tests for security type detection."""
-
- def test_get_security_type_wpa2_eap(self):
- """Test WPA2-EAP detection."""
- assert get_security_type("RSN WPA2-EAP") == "WPA2-EAP"
- assert get_security_type("RSN EAP") == "WPA2-EAP"
-
- def test_get_security_type_wpa_eap(self):
- """Test WPA-EAP detection."""
- assert get_security_type("WPA EAP") == "WPA-EAP"
-
- def test_get_security_type_wpa2_psk(self):
- """Test WPA2-PSK detection."""
- assert get_security_type("RSN HTCAP WME") == "WPA2-PSK"
- # Bare RSN without consumer features is classified as enterprise
- assert get_security_type("RSN") == "WPA2-EAP"
-
- def test_get_security_type_wpa_psk(self):
- """Test WPA-PSK detection."""
- assert get_security_type("WPA HTCAP") == "WPA-PSK"
-
- def test_get_security_type_wep(self):
- """Test WEP detection."""
- assert get_security_type("WEP") == "WEP"
- assert get_security_type("PRIVACY") == "WEP"
-
- def test_get_security_type_open(self):
- """Test open network detection."""
- assert get_security_type("") == "OPEN"
- assert get_security_type("ESS") == "OPEN"
-
-
-class TestCertificateValidation:
- """Tests for certificate validation."""
-
- def test_validate_certificate_empty_path(self):
- """Test validation with empty path."""
- is_valid, error = validate_certificate("")
- assert is_valid is False
- assert "No certificate path" in error
-
- def test_validate_certificate_none_path(self):
- """Test validation with None path."""
- is_valid, error = validate_certificate(None)
- assert is_valid is False
-
- def test_validate_certificate_nonexistent(self):
- """Test validation with nonexistent file."""
- is_valid, error = validate_certificate("/nonexistent/cert.pem")
- assert is_valid is False
- assert "not found" in error
-
- def test_validate_certificate_directory(self):
- """Test validation with directory instead of file."""
- is_valid, error = validate_certificate("/tmp")
- assert is_valid is False
- assert "Not a file" in error
-
- def test_validate_certificate_valid_file(self):
- """Test validation with valid readable file."""
- with tempfile.NamedTemporaryFile(suffix=".pem", delete=False) as f:
- f.write(b"test certificate content")
- temp_path = f.name
- try:
- is_valid, error = validate_certificate(temp_path)
- assert is_valid is True
- assert error is None
- finally:
- os.unlink(temp_path)
-
-
-class TestEapConfigGeneration:
- """Tests for EAP configuration generation."""
-
- def test_generate_eap_config_peap(self):
- """Test PEAP configuration generation."""
- config = {
- 'eap_method': 'PEAP',
- 'identity': 'testuser',
- 'password': 'testpass',
- 'phase2': 'MSCHAPV2',
- }
- result = generate_eap_config("TestNetwork", config)
-
- assert 'ssid="TestNetwork"' in result
- assert 'key_mgmt=WPA-EAP' in result
- assert 'eap=PEAP' in result
- assert 'identity="testuser"' in result
- assert 'password="testpass"' in result
- assert 'phase2="auth=MSCHAPV2"' in result
-
- def test_generate_eap_config_ttls(self):
- """Test TTLS configuration generation."""
- config = {
- 'eap_method': 'TTLS',
- 'identity': 'user@domain.com',
- 'password': 'secret',
- 'phase2': 'PAP',
- 'ca_cert': '/etc/ssl/certs/ca.pem',
- }
- result = generate_eap_config("CorpWifi", config)
-
- assert 'eap=TTLS' in result
- assert 'identity="user@domain.com"' in result
- assert 'phase2="auth=PAP"' in result
- assert 'ca_cert="/etc/ssl/certs/ca.pem"' in result
-
- def test_generate_eap_config_tls(self):
- """Test TLS (certificate-based) configuration generation."""
- config = {
- 'eap_method': 'TLS',
- 'identity': 'client@corp.com',
- 'client_cert': '/etc/ssl/client.pem',
- 'private_key': '/etc/ssl/client.key',
- 'private_key_passwd': 'keypass',
- 'ca_cert': '/etc/ssl/ca.pem',
- }
- result = generate_eap_config("SecureNet", config)
-
- assert 'eap=TLS' in result
- assert 'identity="client@corp.com"' in result
- assert 'client_cert="/etc/ssl/client.pem"' in result
- assert 'private_key="/etc/ssl/client.key"' in result
- assert 'private_key_passwd="keypass"' in result
- assert 'password=' not in result # TLS doesn't use password
-
- def test_generate_eap_config_with_anonymous_identity(self):
- """Test configuration with anonymous identity."""
- config = {
- 'eap_method': 'PEAP',
- 'identity': 'realuser',
- 'password': 'pass',
- 'anonymous_identity': 'anonymous@domain.com',
- }
- result = generate_eap_config("AnonNet", config)
-
- assert 'anonymous_identity="anonymous@domain.com"' in result
-
- def test_generate_eap_config_with_domain_match(self):
- """Test configuration with domain suffix match."""
- config = {
- 'eap_method': 'PEAP',
- 'identity': 'user',
- 'password': 'pass',
- 'domain_suffix_match': 'radius.company.com',
- }
- result = generate_eap_config("SecNet", config)
-
- assert 'domain_suffix_match="radius.company.com"' in result
-
-
-class TestEapConstants:
- """Tests for EAP-related constants."""
-
- def test_eap_methods_defined(self):
- """Test that EAP methods are defined."""
- assert 'PEAP' in EAP_METHODS
- assert 'TTLS' in EAP_METHODS
- assert 'TLS' in EAP_METHODS
-
- def test_phase2_methods_defined(self):
- """Test that Phase 2 methods are defined."""
- assert 'MSCHAPV2' in PHASE2_METHODS
- assert 'GTC' in PHASE2_METHODS
- assert 'PAP' in PHASE2_METHODS
-
- def test_default_ca_cert_path(self):
- """Test default CA certificate path is set."""
- assert DEFAULT_CA_CERT == '/etc/ssl/certs/ca-root-nss.crt'
diff --git a/tests/unit/test_net_api.py b/tests/unit/test_net_api.py
deleted file mode 100644
index b042af6..0000000
--- a/tests/unit/test_net_api.py
+++ /dev/null
@@ -1,105 +0,0 @@
-import sys
-from pathlib import Path
-from subprocess import Popen
-import subprocess
-
-import pytest
-
-top_dir = str(Path(__file__).absolute().parent.parent.parent)
-
-try:
- from src.net_api import (
- card_online,
- connectionStatus,
- connectToSsid,
- defaultcard,
- delete_ssid_wpa_supplicant_config,
- disableWifi,
- enableWifi,
- networkdictionary,
- openrc,
- startallnetwork,
- startnetworkcard,
- stopallnetwork,
- stopnetworkcard,
- wifiDisconnection,
- wlan_status,
- )
- import src.net_api
-except ImportError:
- sys.path.append(top_dir)
- from src.net_api import (
- card_online,
- connectionStatus,
- connectToSsid,
- defaultcard,
- delete_ssid_wpa_supplicant_config,
- disableWifi,
- enableWifi,
- networkdictionary,
- openrc,
- startallnetwork,
- startnetworkcard,
- stopallnetwork,
- stopnetworkcard,
- wifiDisconnection,
- wlan_status,
- )
- import src.net_api
-
-
-def test_default_card_returns_str():
- """test for src.net_api.defaultcard"""
- result = defaultcard()
- assert isinstance(result, str)
-
-
-# TODO: mock subprocess to return empty list
-
-def test_card_online():
- net_card = defaultcard()
- result = card_online(net_card)
- assert result
-
-
-def test_card_not_online():
- net_card = "em99"
- result = card_online(net_card)
- assert not result
-
-
-def test_connection_status_card_is_none():
- """test for src.net_api.connectionStatus"""
- card = None
- result = connectionStatus(card)
- assert isinstance(result, str)
- assert "Network card is not enabled" == result
-
-
-def test_connection_status_card_is_default():
- """test for src.net_api.connectionStatus"""
- card = defaultcard()
- result = connectionStatus(card)
- assert isinstance(result, str)
- assert "inet" in result
- assert "netmask" in result
- assert "broadcast" in result
-
-
-def test_connection_status_card_is_wlan_not_connected():
- """test for src.net_api.connectionStatus"""
- card = 'wlan99'
- result = connectionStatus(card)
- assert isinstance(result, str)
- assert f"WiFi {card} not connected" in result
-
-
-def test_connection_status_card_is_wlan_connected():
- """test for src.net_api.connectionStatus"""
- card = 'wlan0'
- result = connectionStatus(card)
- assert isinstance(result, str)
- assert "inet" in result
- assert "ssid" in result
- assert "netmask" in result
- assert "broadcast" in result