mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-07-22 11:59:33 +00:00
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>
This commit is contained in:
parent
f7ce8a7c66
commit
4dda9ad6b9
@ -16,7 +16,7 @@ code-quality:
|
||||
stage: test
|
||||
needs: []
|
||||
script:
|
||||
- python3 -m flake8 --exclude actions/networks container plinth actions/*
|
||||
- python3 -m flake8 container plinth actions/*
|
||||
|
||||
unit-tests:
|
||||
stage: test
|
||||
|
||||
161
actions/networks
161
actions/networks
@ -1,161 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Configure networking for all wired and wireless devices.
|
||||
#
|
||||
# Creates network-manager connections.
|
||||
|
||||
function get-interfaces {
|
||||
WIRED_IFACES=$(nmcli --terse --fields type,device device | grep "^ethernet:" | cut -d: -f2 | sort -V)
|
||||
NO_OF_WIRED_IFACES=$(echo $WIRED_IFACES | wc -w)
|
||||
|
||||
WIRELESS_IFACES=$(nmcli --terse --fields type,device device | grep "^wifi:" | cut -d: -f2 | sort -V)
|
||||
NO_OF_WIRELESS_IFACES=$(echo $WIRELESS_IFACES | wc -w)
|
||||
}
|
||||
|
||||
function add-connection {
|
||||
local connection_name="$1"
|
||||
shift
|
||||
local interface="$1"
|
||||
shift
|
||||
local remaining_arguments="$@"
|
||||
|
||||
already_exists=$(nmcli --terse --fields name,device con show | grep "$connection_name:$interface" || true)
|
||||
if [ -n "$already_exists" ]; then
|
||||
echo "Connection '$connection_name' already exists for device '$interface', not adding."
|
||||
else
|
||||
nmcli con add con-name "$connection_name" ifname "$interface" $remaining_arguments
|
||||
fi
|
||||
}
|
||||
|
||||
function activate-connection {
|
||||
connection_name="$1"
|
||||
nohup nmcli con up "$connection_name" &>/dev/null &
|
||||
}
|
||||
|
||||
function configure-regular-interface {
|
||||
local interface="$1"
|
||||
local zone="$2"
|
||||
local connection_name="FreedomBox WAN"
|
||||
|
||||
# Create n-m connection for a regular interface
|
||||
add-connection "$connection_name" "$interface" type ethernet
|
||||
nmcli con modify "$connection_name" connection.autoconnect TRUE
|
||||
nmcli con modify "$connection_name" connection.zone "$zone"
|
||||
activate-connection "$connection_name"
|
||||
|
||||
echo "Configured interface '$interface' for '$zone' use as '$connection_name'."
|
||||
}
|
||||
|
||||
function configure-shared-interface {
|
||||
local interface="$1"
|
||||
local connection_name="FreedomBox LAN $interface"
|
||||
|
||||
# Create n-m connection for eth1
|
||||
add-connection "$connection_name" "$interface" type ethernet
|
||||
nmcli con modify "$connection_name" connection.autoconnect TRUE
|
||||
nmcli con modify "$connection_name" connection.zone internal
|
||||
|
||||
# Configure this interface to be shared with other computers.
|
||||
# - 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
|
||||
nmcli con modify "$connection_name" ipv4.method shared
|
||||
|
||||
activate-connection "$connection_name"
|
||||
|
||||
echo "Configured interface '$interface' for shared use as '$connection_name'."
|
||||
}
|
||||
|
||||
function configure-wireless-interface {
|
||||
local interface="$1"
|
||||
local connection_name="FreedomBox $interface"
|
||||
local ssid="FreedomBox$interface"
|
||||
local secret="freedombox123"
|
||||
|
||||
add-connection "$connection_name" "$interface" type wifi ssid "$ssid"
|
||||
nmcli con modify "$connection_name" connection.autoconnect TRUE
|
||||
nmcli con modify "$connection_name" connection.zone internal
|
||||
nmcli con modify "$connection_name" ipv4.method shared
|
||||
nmcli con modify "$connection_name" wifi.mode ap
|
||||
nmcli con modify "$connection_name" wifi-sec.key-mgmt wpa-psk
|
||||
nmcli con modify "$connection_name" wifi-sec.psk "$secret"
|
||||
activate-connection "$connection_name"
|
||||
|
||||
echo "Configured interface '$interface' for shared use as '$connection_name'."
|
||||
}
|
||||
|
||||
function multi-wired-setup {
|
||||
local first_interface="$1"
|
||||
shift
|
||||
local remaining_interfaces="$@"
|
||||
|
||||
configure-regular-interface "$first_interface" external
|
||||
|
||||
for interface in $remaining_interfaces
|
||||
do
|
||||
configure-shared-interface "$interface"
|
||||
done
|
||||
}
|
||||
|
||||
function one-wired-setup {
|
||||
local interface="$1"
|
||||
|
||||
case $NO_OF_WIRELESS_IFACES in
|
||||
"0")
|
||||
configure-regular-interface "$interface" internal
|
||||
;;
|
||||
*)
|
||||
configure-regular-interface "$interface" external
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function wireless-setup {
|
||||
local interfaces="$@"
|
||||
|
||||
for interface in $interfaces
|
||||
do
|
||||
configure-wireless-interface "$interface"
|
||||
done
|
||||
}
|
||||
|
||||
function setup {
|
||||
echo "Setting up network configuration..."
|
||||
get-interfaces
|
||||
|
||||
case $NO_OF_WIRED_IFACES in
|
||||
"0")
|
||||
echo "No wired interfaces detected."
|
||||
;;
|
||||
"1")
|
||||
one-wired-setup $WIRED_IFACES
|
||||
;;
|
||||
*)
|
||||
multi-wired-setup $WIRED_IFACES
|
||||
esac
|
||||
|
||||
wireless-setup $WIRELESS_IFACES
|
||||
|
||||
echo "Done setting up network configuration."
|
||||
}
|
||||
|
||||
#
|
||||
# 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 [ -f "/var/lib/freedombox/is-freedombox-disk-image" ]
|
||||
then
|
||||
setup
|
||||
else
|
||||
echo "Not a FreedomBox disk image. Skipping network configuration."
|
||||
fi
|
||||
@ -1,18 +1,17 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
FreedomBox app to interface with network-manager.
|
||||
"""
|
||||
"""FreedomBox app to interface with network-manager."""
|
||||
|
||||
import subprocess
|
||||
from logging import Logger
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import actions
|
||||
from plinth import app as app_module
|
||||
from plinth import daemon, kvstore, menu, network
|
||||
from plinth.package import Packages
|
||||
|
||||
from . import privileged
|
||||
|
||||
first_boot_steps = [
|
||||
{
|
||||
'id': 'network_topology_wizard',
|
||||
@ -83,7 +82,7 @@ class NetworksApp(app_module.App):
|
||||
def setup(self, old_version):
|
||||
"""Install and configure the app."""
|
||||
super().setup(old_version)
|
||||
actions.superuser_run('networks')
|
||||
privileged.setup()
|
||||
self.enable()
|
||||
|
||||
|
||||
|
||||
189
plinth/modules/networks/privileged.py
Normal file
189
plinth/modules/networks/privileged.py
Normal file
@ -0,0 +1,189 @@
|
||||
# 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.')
|
||||
28
plinth/modules/networks/tests/test_privileged.py
Normal file
28
plinth/modules/networks/tests/test_privileged.py
Normal file
@ -0,0 +1,28 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Test privileged method for networks app."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from .. import privileged
|
||||
|
||||
|
||||
@patch('subprocess.check_output')
|
||||
def test_get_interfaces(check_output):
|
||||
"""Test returning list of network interfaces in sorted order."""
|
||||
check_output.return_value = '\n'.join([
|
||||
'ethernet:ve-fbx-testing',
|
||||
'ethernet:enp39s0',
|
||||
'ethernet:enp32s1',
|
||||
'ethernet:enp4s1',
|
||||
'bridge:virbr0',
|
||||
'wifi:wlp41s0',
|
||||
'loopback:lo',
|
||||
]).encode()
|
||||
|
||||
interfaces = privileged._get_interfaces()
|
||||
assert interfaces == {
|
||||
'ethernet': ['enp4s1', 'enp32s1', 'enp39s0', 've-fbx-testing'],
|
||||
'bridge': ['virbr0'],
|
||||
'wifi': ['wlp41s0'],
|
||||
'loopback': ['lo']
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user