Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 168 additions & 45 deletions NetworkMgr/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
restart_card_network,
restart_routing_and_dhcp,
start_static_network,
start_static_ipv6_network,
enable_slaac,
disable_slaac,
wait_inet
)
from NetworkMgr.query import get_interface_settings
from NetworkMgr.query import get_interface_settings, get_interface_settings_ipv6
from subprocess import run

rcconf = open('/etc/rc.conf', 'r').read()
Expand Down Expand Up @@ -46,20 +49,28 @@ def edit_ipv4_setting(self, widget):
self.saveButton.set_sensitive(True)

def edit_ipv6_setting(self, widget, value):
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.saveButton.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.saveButton.set_sensitive(True)
if widget.get_active():
self.method6 = value
# Check if GUI elements exist (may be called during init)
if not hasattr(self, 'ipInputAddressEntry6'):
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)
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)
# Enable save button if method changed
if self.method6 == self.currentSettings6["Assignment Method"]:
self.saveButton.set_sensitive(False)
else:
self.saveButton.set_sensitive(True)

def entry_trigger_save_button(self, widget, event):
self.saveButton.set_sensitive(True)
Expand Down Expand Up @@ -92,6 +103,9 @@ def __init__(self, selected_nic=None):
interfaceComboBox.set_active(active_index)
self.currentSettings = get_interface_settings(DEFAULT_NIC)
self.method = self.currentSettings["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)

# Build Label to sit in front of the ComboBox
Expand Down Expand Up @@ -267,31 +281,37 @@ def __init__(self, selected_nic=None):
interfaceBox6.pack_start(labelOne6, False, False, 0)
interfaceBox6.pack_end(interfaceComboBox6, True, True, 0)

# Add radio button to toggle DHCP or not
rb_slaac6 = Gtk.RadioButton.new_with_label(None, "SLAAC")
rb_slaac6.set_margin_top(15)
rb_slaac6.connect("toggled", self.edit_ipv6_setting, "SLAAC")
rb_manual6 = Gtk.RadioButton.new_with_label_from_widget(
rb_slaac6, "Manual")
rb_manual6.set_margin_top(15)
rb_manual6.join_group(rb_slaac6)
rb_manual6.connect("toggled", self.edit_ipv6_setting, "Manual")

radioButtonLabel6 = Gtk.Label(label="IPv4 Method:")
# Add radio button to toggle SLAAC or Manual
self.rb_slaac6 = Gtk.RadioButton.new_with_label(None, "SLAAC")
self.rb_slaac6.set_margin_top(15)
self.rb_slaac6.connect("toggled", self.edit_ipv6_setting, "SLAAC")
self.rb_manual6 = Gtk.RadioButton.new_with_label_from_widget(
self.rb_slaac6, "Manual")
self.rb_manual6.set_margin_top(15)
self.rb_manual6.join_group(self.rb_slaac6)
self.rb_manual6.connect("toggled", self.edit_ipv6_setting, "Manual")

# Set initial state based on current settings
if self.method6 == "Manual":
self.rb_manual6.set_active(True)
else:
self.rb_slaac6.set_active(True)

radioButtonLabel6 = Gtk.Label(label="IPv6 Method:")
radioButtonLabel6.set_margin_top(15)
radioButtonLabel6.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(rb_slaac6, True, False, 0)
radioBox6.pack_end(rb_manual6, True, True, 0)
radioBox6.pack_start(self.rb_slaac6, True, False, 0)
radioBox6.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="Subnet Mask")
ipInputMaskLabel6 = Gtk.Label(label="Prefix Length")
ipInputMaskLabel6.set_margin_top(15)

ipInputGatewayLabel6 = Gtk.Label(label="Gateway")
Expand All @@ -301,7 +321,7 @@ def __init__(self, selected_nic=None):
self.ipInputAddressEntry6.set_margin_start(15)
self.ipInputAddressEntry6.connect("key-release-event", self.entry_trigger_save_button)
self.ipInputMaskEntry6 = Gtk.Entry()
self.ipInputAddressEntry6.connect("key-release-event", self.entry_trigger_save_button)
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)
Expand Down Expand Up @@ -353,11 +373,13 @@ def __init__(self, selected_nic=None):
searchBox6.pack_start(searchLabel6, False, False, 0)
searchBox6.pack_end(self.searchEntry6, True, True, 0)

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)
# 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)
Expand All @@ -370,7 +392,6 @@ def __init__(self, selected_nic=None):
gridOne6.attach(ipEntryBox6, 0, 3, 4, 1)
gridOne6.attach(dnsEntryBox6, 0, 4, 4, 1)
gridOne6.attach(searchBox6, 0, 5, 4, 1)
gridOne6.set_sensitive(False)

# Build Notebook

Expand Down Expand Up @@ -401,7 +422,7 @@ def __init__(self, selected_nic=None):

# Apply Tab 2 content and formatting to the notebook
nb.append_page(gridOne6)
nb.set_tab_label_text(gridOne6, "IPv6 Settings WIP")
nb.set_tab_label_text(gridOne6, "IPv6 Settings")
# Put all the widgets together into one window
mainBox = Gtk.Box(orientation=1, spacing=0)
mainBox.pack_start(nb, True, True, 0)
Expand All @@ -412,8 +433,11 @@ def __init__(self, selected_nic=None):
# 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.
self.currentSettings = get_interface_settings(nics[widget.get_active()])
selected_nic = nics[widget.get_active()]
self.currentSettings = get_interface_settings(selected_nic)
self.currentSettings6 = 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"])
Expand All @@ -427,6 +451,28 @@ def update_interface_settings(self):
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")
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)
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.hide_window()
GLib.idle_add(self.update_system)
Expand Down Expand Up @@ -479,8 +525,81 @@ def update_system(self):
wait_inet(nic)
restart_routing_and_dhcp(nic)

# Apply IPv6 configuration
self.update_system_ipv6(nic)

self.destroy()

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()

if self.method6 == 'Manual':
# Static IPv6 configuration
ifconfig_ipv6 = f'ifconfig_{nic}_ipv6="inet6 {inet6} prefixlen {prefixlen}"'
self.update_rc_conf(ifconfig_ipv6)

# Disable rtsold for static configuration
self.update_rc_conf('rtsold_enable="NO"')

# Apply the static IPv6 address
if inet6:
disable_slaac(nic)
start_static_ipv6_network(nic, inet6, prefixlen)

# Set IPv6 default gateway if provided
if gateway6:
# Link-local addresses (fe80::) need interface suffix
if gateway6.lower().startswith('fe80:') and '%' not in gateway6:
gateway6_full = f'{gateway6}%{nic}'
else:
gateway6_full = gateway6
# Save with interface suffix in rc.conf for persistence
self.update_rc_conf(f'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)

# 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)

# Enable rtsold for SLAAC
self.update_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

# Enable SLAAC
enable_slaac(nic)

def add_ipv6_dns(self, dns6):
"""Add IPv6 DNS server to resolv.conf without removing existing entries."""
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')
Comment on lines +586 to +595
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): add_ipv6_dns blindly appends nameserver lines without validating IPv6 format or deduplicating by address family.

Because this only checks for exact text matches, changing the IPv6 DNS leaves the old entry in place and appends a new one. There’s also no validation of dns6, so invalid IPv6 strings could end up in resolv.conf. Consider validating that dns6 is a well-formed IPv6 address and either replacing/pruning existing IPv6 nameserver entries or updating the first one instead of always appending.

Suggested implementation:

    def add_ipv6_dns(self, dns6):
        """
        Ensure resolv.conf has a single, valid IPv6 DNS nameserver entry.

        - Validate dns6 as an IPv6 address.
        - Replace the first existing IPv6 nameserver with dns6 and remove any
          additional IPv6 nameserver entries.
        - If no IPv6 nameserver exists, append one.
        """
        resolv_path = '/etc/resolv.conf'

        try:
            # Validate that dns6 is a well-formed IPv6 address
            try:
                import ipaddress
                ip = ipaddress.ip_address(dns6)
                if ip.version != 6:
                    return  # Not IPv6, do nothing
            except Exception:
                # Invalid IP or ipaddress not available; avoid writing bad entries
                return

            try:
                with open(resolv_path, 'r') as f:
                    lines = f.readlines()
            except FileNotFoundError:
                lines = []

            new_lines = []
            ipv6_replaced = False

            for line in lines:
                stripped = line.strip()
                if not stripped.startswith('nameserver'):
                    new_lines.append(line)
                    continue

                parts = stripped.split()
                if len(parts) != 2:
                    # Keep malformed or unexpected lines as-is
                    new_lines.append(line)
                    continue

                _, addr = parts

                # Try to classify existing nameserver IP (IPv4 vs IPv6)
                try:
                    existing_ip = ipaddress.ip_address(addr)
                except ValueError:
                    # If it doesn't parse, preserve it
                    new_lines.append(line)
                    continue

                if existing_ip.version == 4:
                    # Always keep IPv4 nameservers untouched
                    new_lines.append(line)
                else:
                    # IPv6 nameserver: replace the first, drop the rest
                    if not ipv6_replaced:
                        new_lines.append(f'nameserver {dns6}\n')
                        ipv6_replaced = True
                    # Subsequent IPv6 nameservers are skipped (pruned)

            if not ipv6_replaced:
                # No existing IPv6 nameserver: append a new one
                new_lines.append(f'nameserver {dns6}\n')

            with open(resolv_path, 'w') as f:
                f.writelines(new_lines)
        except Exception:
            # resolv.conf may be managed by dhclient or be otherwise immutable
            pass

To keep imports clean and avoid importing inside the function, consider adding at the top of NetworkMgr/configuration.py:

<<<<<<< SEARCH

existing imports...

=======

existing imports...

import ipaddress

REPLACE

If you make this top-level import, remove the inner import ipaddress and use the already-imported module inside add_ipv6_dns.

except Exception:
pass # resolv.conf may be managed by dhclient

def remove_rc_conf_var(self, varname):
"""Remove a variable from rc.conf using sysrc."""
run(f'sysrc -x {varname}', shell=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep


def hide_window(self):
self.hide()
return False
Expand All @@ -492,13 +611,17 @@ def update_rc_conf(self, line):
run(f'sysrc {line}', shell=True)

def remove_rc_conf_line(self, line):
with open('/etc/rc.conf', "r+") as rc_conf:
lines = rc_conf.readlines()
rc_conf.seek(0)
idx = lines.index(line)
lines.pop(idx)
rc_conf.truncate()
rc_conf.writelines(lines)
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):
Expand Down
61 changes: 61 additions & 0 deletions NetworkMgr/net_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,67 @@ def start_static_network(netcard, inet, netmask):
run('service routing restart', shell=True)


# 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)


def enable_slaac(netcard):
"""Enable SLAAC (Stateless Address Autoconfiguration) on the interface
by toggling accept_rtadv and soliciting router advertisements."""
# First disable, then re-enable to ensure clean state
run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
sleep(0.5)
# Enable accept_rtadv for SLAAC
run(f'ifconfig {netcard} inet6 accept_rtadv', shell=True)
# Start rtsold to solicit router advertisements
run(f'rtsol {netcard}', shell=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.subprocess-shell-true): Found 'subprocess' function 'run' with 'shell=True'. This is dangerous because this call will spawn the command using a shell process. Doing so propagates current shell settings and variables, which makes it much easier for a malicious actor to execute commands. Use 'shell=False' instead.

Suggested change
run(f'rtsol {netcard}', shell=True)
run(f'rtsol {netcard}', shell=False)

Source: opengrep



def disable_slaac(netcard):
"""Disable SLAAC on the interface."""
run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.subprocess-shell-true): Found 'subprocess' function 'run' with 'shell=True'. This is dangerous because this call will spawn the command using a shell process. Doing so propagates current shell settings and variables, which makes it much easier for a malicious actor to execute commands. Use 'shell=False' instead.

Suggested change
run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=False)

Source: opengrep



def get_ipv6_addresses(netcard):
"""Get all IPv6 addresses configured on the interface.
Returns list of tuples: (address, prefixlen)."""
try:
output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

# 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:
return []


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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.subprocess-shell-true): Found 'subprocess' function 'check_output' with 'shell=True'. This is dangerous because this call will spawn the command using a shell process. Doing so propagates current shell settings and variables, which makes it much easier for a malicious actor to execute commands. Use 'shell=False' instead.

Suggested change
output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
output = check_output(f'ifconfig {netcard}', shell=False, universal_newlines=True)

Source: opengrep

return 'ACCEPT_RTADV' in output
except Exception:
return False


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 startnetworkcard(netcard):
run(f'service netif start {netcard}', shell=True)
sleep(1)
Expand Down
Loading