Skip to content

Conversation

@Kernel-Error
Copy link
Contributor

@Kernel-Error Kernel-Error commented Jan 14, 2026

Summary

  • Implement IPv6 support in the Network Configuration dialog
  • Enable the previously disabled "IPv6 Settings WIP" tab
  • Support both SLAAC and static IPv6 configuration

Changes

New Features

  • SLAAC: Automatic IPv6 configuration via Router Advertisements
  • Static IPv6: Manual address, prefix length, gateway, and DNS configuration
  • Link-local gateway support: Automatically adds interface suffix for fe80:: addresses

Bug Fixes

  • Fix "IPv4 Method" label → "IPv6 Method"
  • Fix "Subnet Mask" label → "Prefix Length"
  • Fix duplicate connect() handler on wrong widget
  • Fix remove_rc_conf_line() ValueError when line doesn't exist
  • Fix edit_ipv6_setting() AttributeError during initialization
  • Fix IPv6 DNS regex matching IPv4 addresses

New Functions

net_api.py:

  • start_static_ipv6_network(netcard, inet6, prefixlen)
  • enable_slaac(netcard) / disable_slaac(netcard)
  • get_ipv6_addresses(netcard)
  • has_slaac_enabled(netcard)
  • get_ipv6_gateway()

query.py:

  • get_interface_settings_ipv6(active_nic)

Test Plan

Tested on GhostBSD 14.3-RELEASE with FritzBox router:

  • Static IPv6 address configuration
  • SLAAC (receives global + ULA addresses)
  • IPv6 gateway with link-local address (fe80::)
  • IPv6 DNS server configuration
  • Persistence after reboot
  • Interface switching in GUI

Screenshots

The IPv6 Settings tab is now functional with SLAAC/Manual radio buttons and input fields for Address, Prefix Length, Gateway, DNS, and Search Domain.

Closes #120

Summary by Sourcery

Add functional IPv6 configuration support to the network manager, including SLAAC and static setup in the GUI and backend helpers.

New Features:

  • Expose a fully functional IPv6 Settings tab with SLAAC and manual configuration options tied to per-interface state.
  • Introduce backend helpers to configure static IPv6 addresses, control SLAAC, and query IPv6 addresses and gateway information.
  • Provide IPv6-specific interface querying to populate the UI from rc.conf, ifconfig, routing table, and resolv.conf.

Bug Fixes:

  • Correct IPv6-related labels and widget signal wiring in the configuration dialog.
  • Prevent crashes when editing IPv6 settings during initialization or when rc.conf lines are missing.
  • Tighten DNS parsing to only treat IPv6-style entries as IPv6 DNS servers.

Enhancements:

  • Ensure IPv6 GUI controls reflect and update current interface assignment method and field sensitivity.
  • Safely manage rc.conf variables and resolv.conf updates when toggling between manual IPv6 and SLAAC modes.

Implement IPv6 support in the Network Configuration dialog:

- Add SLAAC (Stateless Address Autoconfiguration) support
- Add static IPv6 address configuration
- Add IPv6 gateway configuration (with link-local address support)
- Add IPv6 DNS server configuration
- Fix existing GUI bugs (labels, connect handlers)

New functions in net_api.py:
- start_static_ipv6_network()
- enable_slaac() / disable_slaac()
- get_ipv6_addresses()
- has_slaac_enabled()
- get_ipv6_gateway()

New function in query.py:
- get_interface_settings_ipv6()

Tested on GhostBSD 14.3-RELEASE with FritzBox router.

Closes ghostbsd#120
@Kernel-Error Kernel-Error requested review from a team as code owners January 14, 2026 12:47
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 14, 2026

Reviewer's Guide

Implements full IPv6 configuration support in the Network Configuration dialog, including SLAAC and static IPv6 workflows, wiring the GUI to new query and net API helpers, and hardening rc.conf/resolv.conf handling and labels for IPv6-specific semantics.

Sequence diagram for applying IPv6 settings from the GUI

sequenceDiagram
    actor User
    participant ConfigWindow as NetworkConfigurationWindow
    participant Query as query
    participant NetAPI as net_api
    participant RcConf as rc.conf
    participant Resolv as resolv.conf
    participant Ifconfig as ifconfig_route_netstat

    User->>ConfigWindow: Launch NetworkMgr
    ConfigWindow->>Query: get_interface_settings(active_nic)
    ConfigWindow->>Query: get_interface_settings_ipv6(active_nic)
    Query-->>ConfigWindow: IPv4 and IPv6 settings
    ConfigWindow->>ConfigWindow: update_interface_settings_ipv6
    ConfigWindow->>ConfigWindow: set rb_slaac6 or rb_manual6

    User->>ConfigWindow: Change IPv6 Method radio (SLAAC/Manual)
    ConfigWindow->>ConfigWindow: edit_ipv6_setting
    ConfigWindow->>ConfigWindow: Toggle sensitivity of IPv6 inputs

    User->>ConfigWindow: Edit IPv6 address, prefix, gateway, DNS
    User->>ConfigWindow: Click Save
    ConfigWindow->>ConfigWindow: commit_pending_changes
    ConfigWindow->>ConfigWindow: update_system

    ConfigWindow->>ConfigWindow: update_system_ipv6(nic)
    alt Method Manual
        ConfigWindow->>RcConf: update_rc_conf(ifconfig_nic_ipv6="inet6 inet6 prefixlen prefixlen")
        ConfigWindow->>RcConf: update_rc_conf(rtsold_enable="NO")
        opt IPv6 address provided
            ConfigWindow->>NetAPI: disable_slaac(nic)
            NetAPI->>Ifconfig: ifconfig nic inet6 -accept_rtadv
            ConfigWindow->>NetAPI: start_static_ipv6_network(nic, inet6, prefixlen)
            NetAPI->>Ifconfig: ifconfig nic inet6 inet6 prefixlen prefixlen
            NetAPI->>Ifconfig: service routing restart
        end
        opt Gateway provided
            ConfigWindow->>ConfigWindow: Add %nic suffix for fe80:: gateway
            ConfigWindow->>RcConf: update_rc_conf(ipv6_defaultrouter="gateway6_full")
            ConfigWindow->>Ifconfig: route delete -inet6 default
            ConfigWindow->>Ifconfig: route add -inet6 default gateway6_full
        end
        opt DNS provided
            ConfigWindow->>Resolv: add_ipv6_dns(dns6)
            Resolv->>Resolv: Append nameserver dns6
        end
    else Method SLAAC
        ConfigWindow->>RcConf: update_rc_conf(ifconfig_nic_ipv6="inet6 accept_rtadv")
        ConfigWindow->>RcConf: update_rc_conf(rtsold_enable="YES")
        ConfigWindow->>ConfigWindow: remove_rc_conf_var(ipv6_defaultrouter)
        ConfigWindow->>Ifconfig: sysrc -x ipv6_defaultrouter
        ConfigWindow->>NetAPI: enable_slaac(nic)
        NetAPI->>Ifconfig: ifconfig nic inet6 -accept_rtadv
        NetAPI->>Ifconfig: ifconfig nic inet6 accept_rtadv
        NetAPI->>Ifconfig: rtsol nic
    end

    ConfigWindow-->>User: Window closes, IPv6 config applied
Loading

Flow diagram for IPv6 method handling and gateway logic

flowchart TD
    Start[Start IPv6 update_system_ipv6] --> ReadInputs[Read GUI values inet6, prefixlen, gateway6, dns6, method6]
    ReadInputs --> CheckMethod{Assignment method Manual?}

    CheckMethod -- Yes --> ManualCfg[Prepare static IPv6 config ifconfig_nic_ipv6]
    ManualCfg --> RcConfStatic[update_rc_conf ifconfig_nic_ipv6]
    RcConfStatic --> DisableRtsold[update_rc_conf rtsold_enable=NO]

    DisableRtsold --> HasInet6{inet6 non empty?}
    HasInet6 -- Yes --> DisableSlaac[disable_slaac on interface]
    DisableSlaac --> ApplyStatic[call start_static_ipv6_network inet6 prefixlen]
    HasInet6 -- No --> CheckGateway

    ApplyStatic --> CheckGateway{Gateway provided?}
    CheckGateway -- Yes --> GatewayType{Gateway starts with fe80:: and no %?}
    GatewayType -- Yes --> AddSuffix[Append %nic to gateway6]
    GatewayType -- No --> UseGateway[Use gateway6 as is]

    AddSuffix --> SaveGwRcConf[update_rc_conf ipv6_defaultrouter=gateway6_full]
    UseGateway --> SaveGwRcConf
    SaveGwRcConf --> RouteDelete[route delete -inet6 default]
    RouteDelete --> RouteAdd[route add -inet6 default gateway6_full]

    CheckGateway -- No --> CheckDNS{DNS provided?}
    RouteAdd --> CheckDNS

    CheckDNS -- Yes --> AddDns[add_ipv6_dns dns6 to resolv.conf]
    CheckDNS -- No --> EndManual[End Manual path]
    AddDns --> EndManual

    CheckMethod -- No --> SlaacCfg[Prepare SLAAC config ifconfig_nic_ipv6=accept_rtadv]
    SlaacCfg --> RcConfSlaac[update_rc_conf ifconfig_nic_ipv6]
    RcConfSlaac --> EnableRtsold[update_rc_conf rtsold_enable=YES]
    EnableRtsold --> RemoveStaticGw[remove_rc_conf_var ipv6_defaultrouter]
    RemoveStaticGw --> EnableSlaac[enable_slaac on interface]
    EnableSlaac --> EndSlaac[End SLAAC path]

    EndManual --> End[Done]
    EndSlaac --> End
Loading

File-Level Changes

Change Details Files
Wire IPv6 GUI controls to actual configuration state and behavior (SLAAC vs Manual) and ensure they initialize and toggle correctly.
  • Extend dialog init to load IPv6 settings via get_interface_settings_ipv6 and track method6/currentSettings6
  • Make IPv6 radio buttons instance attributes, set their active state from current settings, and update field sensitivity based on Manual vs SLAAC
  • Fix edit_ipv6_setting to guard against early init calls, update method6 only when toggles are active, and enable/disable Save depending on method change
  • Initialize IPv6 field sensitivity based on current method instead of hard‑disabling the whole IPv6 grid
  • Refresh IPv6 settings when NIC selection changes using update_interface_settings_ipv6
NetworkMgr/configuration.py
Add logic to display and manage IPv6-specific fields and labels correctly in the UI.
  • Change IPv6 method label text from IPv4-specific wording to 'IPv6 Method:'
  • Change mask label from 'Subnet Mask' to 'Prefix Length' to match IPv6 semantics
  • Fix key-release handler hookup for the IPv6 prefix entry so edits trigger Save
  • Rename IPv6 tab from 'IPv6 Settings WIP' to 'IPv6 Settings' and fully enable it
NetworkMgr/configuration.py
Implement application of IPv6 configuration (static and SLAAC) to the system, including rc.conf, routing, and DNS updates.
  • Add update_system_ipv6 to apply either Manual or SLAAC IPv6 configuration based on method6 and call it from update_system
  • For Manual mode, write ifconfig__ipv6 inet6/prefixlen to rc.conf, disable rtsold, call start_static_ipv6_network, and set ipv6_defaultrouter (handling link-local fe80:: by appending interface suffix) and route table
  • For Manual mode, append IPv6 DNS servers to resolv.conf if not already present via add_ipv6_dns
  • For SLAAC mode, write accept_rtadv ifconfig line, enable rtsold, remove any static ipv6_defaultrouter via sysrc, and call enable_slaac
  • Introduce remove_rc_conf_var helper using sysrc -x and harden remove_rc_conf_line to be no-op when the target line or file is missing
NetworkMgr/configuration.py
NetworkMgr/net_api.py
Provide backend helpers to manage IPv6 addresses, SLAAC state, and gateway discovery.
  • Add start_static_ipv6_network to configure a static inet6 address and restart routing
  • Add enable_slaac/disable_slaac helpers to toggle accept_rtadv and run rtsol for router advertisements
  • Add get_ipv6_addresses to parse configured IPv6 addresses from ifconfig output, and has_slaac_enabled to detect ACCEPT_RTADV
  • Add get_ipv6_gateway to extract the default IPv6 route from netstat -rn -f inet6
NetworkMgr/net_api.py
Expose a query API to read effective IPv6 configuration from rc.conf, ifconfig, routing, and resolv.conf for a given interface.
  • Implement get_interface_settings_ipv6 to derive Assignment Method from ifconfig__ipv6 (accept_rtadv vs static) with SLAAC as default
  • Parse current global (non-link-local) IPv6 address and prefix length from ifconfig, falling back to empty/64 when absent or on error
  • Derive IPv6 default gateway first from ipv6_defaultrouter in rc.conf, then from netstat routing table, stripping any interface suffix
  • Extract IPv6 DNS servers and shared search/domain from resolv.conf with IPv6-aware regexes that do not match IPv4 addresses
NetworkMgr/query.py

Assessment against linked issues

Issue Objective Addressed Explanation
#120 Implement functional IPv6 configuration in NetworkMgr (GUI and backend), including enabling/disabling SLAAC and configuring static IPv6 address, prefix length, and gateway with persistence via rc.conf.
#120 Add IPv6 DNS server configuration support (both reading existing configuration and writing new DNS servers).
#120 Fix IPv6 tab label issues: change 'IPv4 Method:' to 'IPv6 Method:' and 'Subnet Mask' to 'Prefix Length' in the IPv6 settings UI.

Possibly linked issues

  • #N/A: They describe the same IPv6 configuration feature; PR implements SLAAC, static config, labels, and backend support requested.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 19 security issues, 4 other issues, and left some high level feedback:

Security issues:

  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)
  • 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. (link)
  • 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()'. (link)

General comments:

  • In enable_slaac, the comment says you remove existing IPv6 addresses but the code only toggles accept_rtadv; if the intent is truly to clear old IPv6 addresses you may want to explicitly flush existing inet6 addresses or adjust the comment to reflect the actual behavior.
  • The implementation of get_ipv6_addresses conflicts with its docstring: it claims to exclude link-local and localhost addresses but currently returns all inet6 addresses that match the regex; either filter out fe80::/::1 as described or update the docstring to match the current behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `enable_slaac`, the comment says you remove existing IPv6 addresses but the code only toggles `accept_rtadv`; if the intent is truly to clear old IPv6 addresses you may want to explicitly flush existing inet6 addresses or adjust the comment to reflect the actual behavior.
- The implementation of `get_ipv6_addresses` conflicts with its docstring: it claims to exclude link-local and localhost addresses but currently returns all inet6 addresses that match the regex; either filter out `fe80::`/`::1` as described or update the docstring to match the current behavior.

## Individual Comments

### Comment 1
<location> `NetworkMgr/query.py:58-59` </location>
<code_context>
+        ipv6_settings["Prefix Length"] = "64"
+
+    # Get IPv6 default gateway from rc.conf or routing table
+    gateway_search = re.search(
+        r'^ipv6_defaultrouter="([0-9a-fA-F:]+)"',
+        rc_conf,
+        re.MULTILINE
</code_context>

<issue_to_address>
**issue (bug_risk):** ipv6_defaultrouter regex will miss link-local gateways stored with an interface suffix (e.g., fe80::1%em0).

In `update_system_ipv6` link-local gateways are stored with an interface suffix (e.g., `ipv6_defaultrouter="fe80::1%em0"`), but this regex only permits `[0-9a-fA-F:]`, so values with `%<iface>` won’t match and existing gateways won’t appear in the GUI. Please extend the pattern to allow an optional `%iface` suffix and handle it consistently with how the value is written.
</issue_to_address>

### Comment 2
<location> `NetworkMgr/net_api.py:329-337` </location>
<code_context>
+    run('service routing restart', shell=True)
+
+
+def enable_slaac(netcard):
+    """Enable SLAAC (Stateless Address Autoconfiguration) on the interface."""
+    # Remove any existing IPv6 addresses first
+    run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
+    sleep(0.5)
</code_context>

<issue_to_address>
**suggestion:** enable_slaac comment and implementation diverge regarding removal of existing IPv6 addresses.

The code only toggles the `accept_rtadv` flag (`inet6 -accept_rtadv`) and does not actually remove any existing IPv6 addresses as the comment suggests. Please either adjust the comment to match the behavior, or, if you do need to clear stale/static IPv6 addresses, add the explicit address flush logic (and/or document that behavior in an appropriate place).

```suggestion
def enable_slaac(netcard):
    """Enable SLAAC (Stateless Address Autoconfiguration) on the interface by
    toggling accept_rtadv and soliciting router advertisements."""
    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)
```
</issue_to_address>

### Comment 3
<location> `NetworkMgr/net_api.py:345-346` </location>
<code_context>
+    run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
+
+
+def get_ipv6_addresses(netcard):
+    """Get all IPv6 addresses configured on the interface."""
+    try:
+        output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
</code_context>

<issue_to_address>
**suggestion (bug_risk):** get_ipv6_addresses returns all IPv6 addresses but the comment suggests excluding link-local/localhost.

The docs/comments state this should exclude link-local (fe80::/10) and ::1, but the implementation currently returns all `inet6` entries. Please either filter the results to match the documented semantics or update the docs/comments to reflect the actual behavior so callers aren’t misled.
</issue_to_address>

### Comment 4
<location> `NetworkMgr/configuration.py:586-595` </location>
<code_context>
+            # 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')
+        except Exception:
+            pass  # resolv.conf may be managed by dhclient
+
+    def remove_rc_conf_var(self, varname):
</code_context>

<issue_to_address>
**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:

```python
    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`.
</issue_to_address>

### Comment 5
<location> `NetworkMgr/configuration.py:564` </location>
<code_context>
                run(f'route add -inet6 default {gateway6_full}', shell=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 6
<location> `NetworkMgr/configuration.py:564` </location>
<code_context>
                run(f'route add -inet6 default {gateway6_full}', shell=True)
</code_context>

<issue_to_address>
**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.

```suggestion
                run(f'route add -inet6 default {gateway6_full}', shell=False)
```

*Source: opengrep*
</issue_to_address>

### Comment 7
<location> `NetworkMgr/configuration.py:601` </location>
<code_context>
        run(f'sysrc -x {varname}', shell=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 8
<location> `NetworkMgr/configuration.py:601` </location>
<code_context>
        run(f'sysrc -x {varname}', shell=True)
</code_context>

<issue_to_address>
**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.

```suggestion
        run(f'sysrc -x {varname}', shell=False)
```

*Source: opengrep*
</issue_to_address>

### Comment 9
<location> `NetworkMgr/net_api.py:324` </location>
<code_context>
    run(f'ifconfig {netcard} inet6 {inet6} prefixlen {prefixlen}', shell=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 10
<location> `NetworkMgr/net_api.py:324` </location>
<code_context>
    run(f'ifconfig {netcard} inet6 {inet6} prefixlen {prefixlen}', shell=True)
</code_context>

<issue_to_address>
**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.

```suggestion
    run(f'ifconfig {netcard} inet6 {inet6} prefixlen {prefixlen}', shell=False)
```

*Source: opengrep*
</issue_to_address>

### Comment 11
<location> `NetworkMgr/net_api.py:332` </location>
<code_context>
    run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 12
<location> `NetworkMgr/net_api.py:332` </location>
<code_context>
    run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
</code_context>

<issue_to_address>
**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.

```suggestion
    run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=False)
```

*Source: opengrep*
</issue_to_address>

### Comment 13
<location> `NetworkMgr/net_api.py:335` </location>
<code_context>
    run(f'ifconfig {netcard} inet6 accept_rtadv', shell=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 14
<location> `NetworkMgr/net_api.py:335` </location>
<code_context>
    run(f'ifconfig {netcard} inet6 accept_rtadv', shell=True)
</code_context>

<issue_to_address>
**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.

```suggestion
    run(f'ifconfig {netcard} inet6 accept_rtadv', shell=False)
```

*Source: opengrep*
</issue_to_address>

### Comment 15
<location> `NetworkMgr/net_api.py:337` </location>
<code_context>
    run(f'rtsol {netcard}', shell=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 16
<location> `NetworkMgr/net_api.py:337` </location>
<code_context>
    run(f'rtsol {netcard}', shell=True)
</code_context>

<issue_to_address>
**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.

```suggestion
    run(f'rtsol {netcard}', shell=False)
```

*Source: opengrep*
</issue_to_address>

### Comment 17
<location> `NetworkMgr/net_api.py:342` </location>
<code_context>
    run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 18
<location> `NetworkMgr/net_api.py:342` </location>
<code_context>
    run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=True)
</code_context>

<issue_to_address>
**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.

```suggestion
    run(f'ifconfig {netcard} inet6 -accept_rtadv', shell=False)
```

*Source: opengrep*
</issue_to_address>

### Comment 19
<location> `NetworkMgr/net_api.py:348` </location>
<code_context>
        output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 20
<location> `NetworkMgr/net_api.py:348` </location>
<code_context>
        output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
</code_context>

<issue_to_address>
**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.

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

*Source: opengrep*
</issue_to_address>

### Comment 21
<location> `NetworkMgr/net_api.py:359` </location>
<code_context>
        output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
</code_context>

<issue_to_address>
**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*
</issue_to_address>

### Comment 22
<location> `NetworkMgr/net_api.py:359` </location>
<code_context>
        output = check_output(f'ifconfig {netcard}', shell=True, universal_newlines=True)
</code_context>

<issue_to_address>
**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.

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

*Source: opengrep*
</issue_to_address>

### Comment 23
<location> `NetworkMgr/query.py:68-71` </location>
<code_context>
            netstat_output = check_output(
                'netstat -rn -f inet6'.split(),
                universal_newlines=True
            )
</code_context>

<issue_to_address>
**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*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +586 to +595
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')
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.

- Fix ipv6_defaultrouter regex to match link-local with interface suffix
- Fix enable_slaac docstring to match actual behavior
- Fix get_ipv6_addresses docstring (returns all addresses, not filtered)
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

New security issues found


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

"""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

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

Copy link
Member

@ericbsd ericbsd left a comment

Choose a reason for hiding this comment

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

All those Sourcery comments did make it harder to review.

@ericbsd ericbsd merged commit 106d5cc into ghostbsd:master Jan 14, 2026
1 check failed
@ericbsd
Copy link
Member

ericbsd commented Jan 14, 2026

Thanks for the contribution.

@ericbsd ericbsd moved this from In Review to Done in Development Tracker Jan 27, 2026
@ericbsd ericbsd moved this to Done in Development Tracker Jan 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feature] IPv6 Configuration Support

2 participants