Skip to content

feat: add OpenVPN/WireGuard configuration importer, VPN settings editor, compact tray menu, and connection toast notifications - #134

Open
mbnunes wants to merge 10 commits into
ghostbsd:masterfrom
mbnunes:imports
Open

mbnunes wants to merge 10 commits into
ghostbsd:masterfrom
mbnunes:imports

Conversation

@mbnunes

@mbnunes mbnunes commented Nov 10, 2026 •

Copy link
Copy Markdown

Summary

This PR introduces full OpenVPN (.ovpn / .conf) and WireGuard (.conf) configuration import and management to networkmgr (modeled after Linux's NetworkManager-openvpn), alongside a redesigned compact single-row Tray Menu UI and non-blocking desktop popup notifications when connecting or disconnecting networks.

Key Features & Changes

1. OpenVPN & WireGuard Configuration Importer and Editor

  • New OpenVPN Backend (NetworkMgr/openvpn_api.py):
  • Full parser and exporter for OpenVPN .ovpn and .conf profiles (parse_openvpn_config, save_openvpn_config,
    delete_openvpn_config, openvpn_dictionary, enable_openvpn, disable_openvpn).
  • Automatic detection between OpenVPN and WireGuard configuration files (detect_vpn_file_type).
  • Extracts inline PEM blocks (<ca>, <cert>, <key>, <pkcs12>, <tls-auth>, <tls-crypt>, <tls-crypt-v2>,
    <secret>, <extra-certs>, <http-proxy-user-pass>) and <connection> blocks, storing certificates and keys safely with
    0600 permissions under /usr/local/etc/openvpn/certs/<profile_id>/.
  • Supports all four authentication modes (tls, password, tls-password, static-key) and prompts for credentials via
    OpenVPNAuthDialog when needed.
  • Supports Manual/Static IPv4 (ifconfig, route-gateway), Manual/Static IPv6 (ifconfig-ipv6, pull-filter ignore), and custom Static Routes (IPv4 & IPv6) (route, route-ipv6).
  • Preserves and updates custom connection display names (# networkmgr-name:) and handles profile renaming cleanly when
    importing or editing a profile.
  • Checks whether security/openvpn is installed (is_openvpn_installed) and shows a clear warning dialog
    (show_openvpn_not_installed_alert) instead of raising FileNotFoundError when /usr/local/sbin/openvpn is missing.
  • WireGuard Importer & Editor Enhancements (NetworkMgr/wg_api.py):
  • Added parse_wireguard_config, save_wireguard_config, delete_wireguard_config, and default_wireguard_config to
    import, create, edit, and remove WireGuard profiles in /usr/local/etc/wireguard/.
  • Guarded wg and wg-quick subprocess calls against missing binaries (OSError / FileNotFoundError).
  • Tabbed VPN Configuration Dialog (NetworkMgr/vpn_dialog.py):
  • Added import_vpn_file_dialog and VPNConnectionDialog with 6 tabs for OpenVPN (VPN, IPv4 Settings, IPv6
    Settings
    , Advanced, Security & TLS, Proxies) plus WireGuard profile editing.
  • VPN Settings Tab in Network Configuration Window (NetworkMgr/configuration.py):
  • Added a VPN Settings tab to NetCardConfigWindow allowing users to view, Import VPN..., create New OpenVPN,
    Edit, Connect / Disconnect, and Delete VPN profiles directly from the configuration window.

2. Compact Single-Row Tray Menu UI/UX (NetworkMgr/trayicon.py)

  • Consolidated the 3-row menu structure per connection (<Name> Connected + Disable <Name> + Configure <Name>) into a
    clean, single-row layout for OpenVPN, WireGuard, and Ethernet (Wired) connections:
    ● <Connection Name> [Connect / Red X Disconnect] [⚙ Configure]
  • Uses inline SVG icons rendered via GdkPixbuf.PixbufLoader:
  • Status indicator dot (emerald green when connected, neutral outline when disconnected).
  • Action button 1: Green play/connect icon when disconnected; Red ✕ icon when connected.
  • Action button 2: Gear (⚙) icon to open the profile or interface configuration window.
  • Added Import VPN Configuration... directly to the tray menu.

3. Desktop Popup Toast Notifications (NetworkMgr/vpn_dialog.py,

NetworkMgr/trayicon.py, NetworkMgr/configuration.py)

  • Added show_connection_notification(name, connected) using a non-blocking GTK3 Gdk.WindowTypeHint.NOTIFICATION popup toast
    in the top-right corner of the primary monitor:
  • [✓] <Connection Name> Connected (green checkmark icon)
  • [✕] <Connection Name> Disconnected (red X icon)
  • Works out of the box across all GhostBSD desktop environments (MATE, XFCE, Openbox, etc.) without requiring external
    notification daemons, never steals keyboard focus, and auto-dismisses after 3 seconds (or on click).

Files Changed


Testing

  • Imported .ovpn and WireGuard .conf profiles (with both external and inline <ca>, <cert>, <key>, <tls-auth>,
    <tls-crypt> blocks) and verified generated configurations in /usr/local/etc/openvpn/ and /usr/local/etc/wireguard/.
  • Verified profile renaming on import/edit and static IPv4/IPv6 addresses and static routes persistence.
  • Verified warning alert on GhostBSD systems where openvpn is not yet installed.
  • Verified single-row tray menu actions (Connect/Disconnect and Configure buttons) and popup toast notifications across VPN, Wired, and Wi-Fi connections.

@sourcery-ai

sourcery-ai Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Sorry @mbnunes, your pull request is larger than the review limit of 150,000 diff characters

Comment thread NetworkMgr/ipsec_api.py Outdated
os.chmod(target_dir, 0o700)
except OSError:
pass
if old_profile_id and old_profile_id != profile_id:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

original_profile_id is set from the imported file's stem (or New_IPsec_VPN) even when no saved profile has that ID. Importing office.sswan named "Office VPN" while office.conf exists deletes office.conf on save. When the names match, _confirm_overwrite_if_exists treats it as an edit and overwrites without asking. Saving two new profiles in a row does the same. original_profile_id should only be set when editing an existing saved profile.

Also, on rename only the networkmgr .conf is removed. The old ipsec.d/nm-<old>.conf and .secrets (holding the PSK or password) stay included by StrongSwan, and a running nm-<old> SA is never brought down.

Comment thread NetworkMgr/openvpn_api.py
pass

# Fallback: check pgrep for an openvpn process using this config
result = run(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pattern isn't anchored, so profile work matches homework.conf. While homework runs, work shows Connected, enable_openvpn('work') returns success without starting anything, and disable_openvpn('work') kills the homework tunnel. Please anchor on a path separator, e.g. /{re.escape(profile_id)}\.(conf|ovpn)( |$), or track the PID file.

Comment thread NetworkMgr/openvpn_api.py Outdated
os.makedirs(cert_dir, exist_ok=True)
os.chmod(cert_dir, 0o700)
auth_file = os.path.join(cert_dir, 'auth.txt')
_write_secure_file(auth_file, f'{username}\n{password}', 0o600)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A password typed at the connect prompt is written into the same auth.txt the saved config points to. After one connect, both values are on disk, the prompt never appears again and the editor shows the password. A prompted password should go to a temporary file that is removed after OpenVPN reads it, or be passed through the management interface.

Comment thread NetworkMgr/openvpn_api.py
elif cfg.get('key_path'):
key_file = _copy_or_keep_cert(cfg['key_path'], cert_dir, 'client.key', 0o600)

pkcs12_file = ''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pkcs12_inline from the parser is never handled here, only pkcs12_path. An imported .ovpn with an inline <pkcs12> block is saved without a client certificate and fails to connect.

Comment thread NetworkMgr/openvpn_api.py
if _is_safe_extra_line(extra):
lines.append(_clean_ovpn_val(extra))

conf_file = os.path.join(OPENVPN_CONFIG_PATH, f'{profile_id}.conf')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The save always writes <id>.conf. Editing an existing foo.ovpn creates foo.conf next to it. openvpn_dictionary then shows the .ovpn (it sorts last and overwrites the key), so the editor reopens the old content while enable_openvpn starts the .conf. Please remove the other extension, or keep the original file name.

Comment thread NetworkMgr/wg_api.py Outdated
cfg['pre_down'] = val
elif is_internal_profile and key == 'postdown':
cfg['post_down'] = val
elif section == 'peer':

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All [Peer] sections are merged into one: the last PublicKey, Endpoint and PSK win and every AllowedIPs is concatenated. Saving a multi-peer config drops all but one peer and gives it every peer's AllowedIPs. Either keep a list of peers, or refuse to edit multi-peer configs in the GUI.

Comment thread NetworkMgr/trayicon.py
configure_item.connect("activate", self.configuration_window_open, netcard)
self.menu.append(configure_item)
wired_label = _("Wired %s (%s)") % (cardnum, netcard)
if connection_state in ("Connected", "Disabled", "Disconnected"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A wired card in the "Disconnected" state (up, no link or address) now gets Connect (enable_card) instead of the previous Disable, so you can no longer bring that interface down from the tray.

Comment thread NetworkMgr/trayicon.py
if btn2 is not None:
btn2.connect("clicked", lambda _b: _invoke(on_configure))

def _on_button_release(_item, event):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any click on the row, including the name, runs the toggle. One mis-click on a connected wired row stops networking. Could the toggle be limited to the button?

{'network': r[0], 'prefixlen': r[1], 'gateway': r[2]}
for r in self.nic_r6_store
]
save_interface_routes(nic, r4_list, r6_list)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Routes are saved to routes/<nic>.routes and applied only here. Nothing applies them again at boot or on interface restart, so they're lost after a reboot. A route removed from the list is also never deleted from the live routing table.

Comment thread NetworkMgr/ipsec_api.py Outdated

auth_lines = []
if cfg.get('ppp_mschapv2', True):
auth_lines.append(' set link accept mschapv2')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

mschapv2 isn't a valid mpd5 set link accept option (valid: pap, chap-md5, chap-msv1, chap-msv2, eap). Line 799 already adds chap-msv2, so this line can map to chap-msv2 or be dropped.

@github-project-automation github-project-automation Bot moved this from To triage to In review in NetworkMGR Issue Traker Nov 10, 2026
@ericbsd ericbsd linked an issue Nov 10, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review
Status: In review

Development

Successfully merging this pull request may close these issues.

[Feature] NetworkMgr needs VPN support

2 participants