Sunil Mohan Adapa 4dda9ad6b9
networks: Use privileged decorator for actions
Tests:

- Initial setup of during first setup works
  - When there are no wired network interfaces
  - When there is 1 wired network interface
    - When there is one wifi interface. wired network is setup as 'external'
      WAN. (simulated with edit of _get_interfaces())
    - When there are no wifi interfaces. wired network is setup as 'internal'
      WAN.
  - When there are multiple wired network interfaces
    - First one is setup as WAN rest as shared
  - When there is one wifi interface, interface is setup as shared.
  - When there are no wifi interfaces

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2022-10-08 18:52:35 -04:00

190 lines
6.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Configure network manager.
During initial setup, configure networking for all wired and wireless devices
by creating network manager connections.
"""
import collections
import itertools
import logging
import re
import subprocess
from plinth import action_utils
from plinth.actions import privileged
def _sort_interfaces(interfaces: list[str]) -> list[str]:
"""Sort interfaces in a well-defined way: eth0, eth1, eth2, ... eth10."""
def key_func(interface):
parts = re.findall(r'(\D*)(\d*)', interface)
parts = [(string, int(number) if number else number)
for string, number in parts]
return list(itertools.chain(parts))
return sorted(interfaces, key=key_func)
def _get_interfaces() -> dict[str, list[str]]:
"""Return all network interfaces by their type."""
output = subprocess.check_output(
['nmcli', '--terse', '--fields', 'type,device', 'device'])
interfaces = collections.defaultdict(list)
for line in output.decode().splitlines():
type_, _, interface = line.partition(':')
interfaces[type_].append(interface)
for type_ in interfaces:
interfaces[type_] = _sort_interfaces(interfaces[type_])
return interfaces
def _add_connection(connection_name: str, interface: str,
remaining_arguments: list[str]):
"""Add an Ethernet/Wi-Fi connection of type regular or shared."""
output = subprocess.check_output(
['nmcli', '--terse', '--fields', 'name,device', 'con', 'show'])
lines = output.decode().splitlines()
if f'{connection_name}:{interface}' in lines:
logging.info('Connection %s already exists for device %s, not adding.',
connection_name, interface)
else:
subprocess.run([
'nmcli', 'con', 'add', 'con-name', connection_name, 'ifname',
interface
] + remaining_arguments, check=True)
def _activate_connection(connection_name: str):
"""Activate a network connection in a background process."""
subprocess.Popen(['nohup', 'nmcli', 'con', 'up', connection_name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _configure_regular_interface(interface: str, zone: str):
"""Create a connection that is not a shared connection."""
connection_name = 'FreedomBox WAN'
properties = {'connection.autoconnect': 'TRUE', 'connection.zone': zone}
# Create n-m connection for a regular interface
_add_connection(connection_name, interface, ['type', 'ethernet'])
_set_connection_properties(connection_name, properties)
_activate_connection(connection_name)
logging.info('Configured interface %s for %s use as %s.', interface, zone,
connection_name)
def _configure_shared_interface(interface: str):
"""Create a shared connection that has traffic forwarding enabled.
Shared connection means:
- Self-assign an address and network
- Start and manage DNS server (dnsmasq)
- Start and manage DHCP server (dnsmasq)
- Register address with mDNS
- Add firewall rules for NATing from this interface
"""
connection_name = f'FreedomBox LAN {interface}'
properties = {
'connection.autoconnect': 'TRUE',
'connection.zone': 'internal',
'ipv4.method': 'shared'
}
# Create n-m connection for eth1
_add_connection(connection_name, interface, ['type', 'ethernet'])
_set_connection_properties(connection_name, properties)
_activate_connection(connection_name)
logging.info('Configured interface %s for shared use as %s.', interface,
connection_name)
def _set_connection_properties(connection_name: str, properties: dict[str,
str]):
"""Configure property key/values on a connection."""
for key, value in properties.items():
subprocess.run(['nmcli', 'con', 'modify', connection_name, key, value],
check=True)
def _configure_wireless_interface(interface: str):
"""Configure a wireless access point."""
connection_name = f'FreedomBox {interface}'
ssid = f'FreedomBox{interface}'
secret = 'freedombox123'
properties = {
'connection.autoconnect': 'TRUE',
'connection.zone': 'internal',
'ipv4.method': 'shared',
'wifi.mode': 'ap',
'wifi-sec.key-mgmt': 'wpa-psk',
'wifi-sec.psk': secret
}
_add_connection(connection_name, interface, ['type', 'wifi', 'ssid', ssid])
_set_connection_properties(connection_name, properties)
_activate_connection(connection_name)
logging.info('Configured interface %s for shared use as %s', interface,
connection_name)
def _multi_wired_setup(interfaces: list[str]):
"""Configure all Ethernet connections on a system with many of them."""
_configure_regular_interface(interfaces[0], 'external')
for interface in interfaces[1:]:
_configure_shared_interface(interface)
def _one_wired_setup(interface: str, interfaces: dict[str, list[str]]):
"""Configure an Ethernet connection on a system with only one."""
if not len(interfaces['wifi']):
_configure_regular_interface(interface, 'internal')
else:
_configure_regular_interface(interface, 'external')
def _wireless_setup(interfaces: list[str]):
"""Configure all wireless access points."""
for interface in interfaces:
_configure_wireless_interface(interface)
@privileged
def setup():
"""Create network manager connections.
For a user who installed using freedombox-setup Debian package, when
FreedomBox Service (Plinth) is run for the first time, don't run network
setup. This is ensured by checking for the file
/var/lib/freedombox/is-freedombox-disk-image which will not exist.
For a user who installed using FreedomBox disk image, when FreedomBox
Service (Plinth) runs for the first time, setup process executes and
triggers the script due networks module being an essential module.
"""
if not action_utils.is_disk_image():
logging.info(
'Not a FreedomBox disk image. Skipping network configuration.')
return
logging.info('Setting up network configuration...')
interfaces = _get_interfaces()
if len(interfaces['ethernet']) == 0:
logging.info('No wired interfaces detected.')
elif len(interfaces['ethernet']) == 1:
_one_wired_setup(interfaces['ethernet'][0], interfaces)
else:
_multi_wired_setup(interfaces['ethernet'])
_wireless_setup(interfaces['wifi'])
logging.info('Done setting up network configuration.')