Signed-off-by: Hadi <[email protected]>
This commit is contained in:
Hadi
2026-08-13 00:58:37 +02:00
commit 59b31e5ca3
103 changed files with 8577 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
package network
import "strings"
// Device is one row of `nmcli device status`.
type Device struct {
Name string
Type string // e.g. "wifi", "ethernet", "wireguard", "loopback"
State string // e.g. "connected", "disconnected", "unavailable"
Connection string // active connection profile name, empty if none
}
func (d Device) Connected() bool {
return strings.HasPrefix(d.State, "connected")
}
// ListDevices returns every network device known to NetworkManager.
func ListDevices() ([]Device, error) {
rows, err := runTerse("DEVICE,TYPE,STATE,CONNECTION", "device", "status")
if err != nil {
return nil, err
}
devices := make([]Device, 0, len(rows))
for _, row := range rows {
devices = append(devices, Device{
Name: field(row, 0),
Type: field(row, 1),
State: field(row, 2),
Connection: field(row, 3),
})
}
return devices, nil
}
// DeviceConnect asks NetworkManager to bring a device up using whichever
// saved connection profile fits it best. Unlike VPNUp (which reactivates a
// specific, already-known connection by name), this works even right after
// a disconnect, when `device status` no longer reports which connection
// profile was last active on that device.
func DeviceConnect(device string) error {
_, err := run("device", "connect", device)
return err
}
// DeviceIP4 returns the device's primary private IPv4 address without its
// CIDR suffix (e.g. "192.168.1.54"), or "" if the device has none.
func DeviceIP4(device string) (string, error) {
out, err := getField("IP4.ADDRESS", "device", "show", device)
if err != nil {
return "", err
}
if out == "" {
return "", nil
}
// nmcli -g can return multiple addresses newline-separated; keep the first.
if i := strings.IndexByte(out, '\n'); i >= 0 {
out = out[:i]
}
if i := strings.IndexByte(out, '/'); i >= 0 {
out = out[:i]
}
return out, nil
}
// DeviceDetail holds everything settuings shows on the "network info" screen
// for a connected device: addressing, gateway, DNS and hardware details.
type DeviceDetail struct {
HWAddr string
MTU string
IPv4 string // address + CIDR, e.g. "192.168.1.54/24"
Gateway4 string
DNS4 []string
IPv6 []string
Gateway6 string
DNS6 []string
}
// DeviceDetails fetches full addressing/hardware info for device via
// `nmcli device show`. Unlike the tabular nmcli commands elsewhere in this
// package, `device show` prints one "KEY:VALUE" line per property (with
// "KEY[n]" for multi-valued ones) and does not escape colons inside values,
// so it's parsed differently from runTerse.
func DeviceDetails(device string) (DeviceDetail, error) {
out, err := run("-t", "-f",
"GENERAL.HWADDR,GENERAL.MTU,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS",
"device", "show", device)
if err != nil {
return DeviceDetail{}, err
}
var d DeviceDetail
for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
if i := strings.IndexByte(key, '['); i >= 0 {
key = key[:i] // strip the "[n]" multi-value index
}
switch key {
case "GENERAL.HWADDR":
d.HWAddr = value
case "GENERAL.MTU":
d.MTU = value
case "IP4.ADDRESS":
if d.IPv4 == "" {
d.IPv4 = value
}
case "IP4.GATEWAY":
d.Gateway4 = value
case "IP4.DNS":
d.DNS4 = append(d.DNS4, value)
case "IP6.ADDRESS":
d.IPv6 = append(d.IPv6, value)
case "IP6.GATEWAY":
d.Gateway6 = value
case "IP6.DNS":
d.DNS6 = append(d.DNS6, value)
}
}
return d, nil
}
+45
View File
@@ -0,0 +1,45 @@
package network
// EthernetDevice is a wired network interface and its active profile, if any.
type EthernetDevice struct {
Device string
Connection string
State string
IP4 string
}
func (e EthernetDevice) Connected() bool {
return e.State == "connected"
}
// ListEthernet returns every ethernet device, with its private IPv4 address
// filled in when connected.
func ListEthernet() ([]EthernetDevice, error) {
devices, err := ListDevices()
if err != nil {
return nil, err
}
eth := make([]EthernetDevice, 0)
for _, d := range devices {
if d.Type != "ethernet" {
continue
}
e := EthernetDevice{
Device: d.Name,
Connection: d.Connection,
State: d.State,
}
if e.Connected() {
e.IP4, _ = DeviceIP4(d.Name)
}
eth = append(eth, e)
}
return eth, nil
}
// EthernetDown deactivates an ethernet device.
func EthernetDown(device string) error {
_, err := run("device", "disconnect", device)
return err
}
+62
View File
@@ -0,0 +1,62 @@
package network
import "strings"
// KnownNetwork is a saved Wi-Fi connection profile ("known network" in the
// iOS sense), independent of whether its AP is currently in range.
type KnownNetwork struct {
Name string
Security string
AutoConnect bool
Active bool
}
// ListKnownWifi returns every saved Wi-Fi connection profile.
func ListKnownWifi() ([]KnownNetwork, error) {
rows, err := runTerse("NAME,TYPE,AUTOCONNECT,ACTIVE", "connection", "show")
if err != nil {
return nil, err
}
known := make([]KnownNetwork, 0)
for _, row := range rows {
if field(row, 1) != "802-11-wireless" {
continue
}
name := field(row, 0)
security, _ := getField("802-11-wireless-security.key-mgmt", "connection", "show", "id", name)
known = append(known, KnownNetwork{
Name: name,
Security: security,
AutoConnect: field(row, 2) == "yes",
Active: field(row, 3) == "yes",
})
}
return known, nil
}
// Secret returns the saved PSK/passphrase for a Wi-Fi connection profile.
// It is empty for open networks or profiles using a non-PSK auth method.
func Secret(name string) (string, error) {
out, err := run("--show-secrets", "-g", "802-11-wireless-security.psk", "connection", "show", "id", name)
if err != nil {
return "", err
}
return strings.TrimSpace(out), nil
}
// SetAutoConnect toggles whether a saved connection is joined automatically.
func SetAutoConnect(name string, enabled bool) error {
value := "no"
if enabled {
value = "yes"
}
_, err := run("connection", "modify", "id", name, "connection.autoconnect", value)
return err
}
// Forget deletes a saved connection profile entirely.
func Forget(name string) error {
_, err := run("connection", "delete", "id", name)
return err
}
+120
View File
@@ -0,0 +1,120 @@
// Package network wraps nmcli to read and control network state
// (Wi-Fi, Ethernet, VPN). Every exported function shells out to nmcli;
// there is no direct D-Bus/NetworkManager integration.
package network
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
"time"
)
var (
ErrNotFound = errors.New("nmcli not found in PATH")
ErrNotRunning = errors.New("NetworkManager is not running")
)
const defaultTimeout = 15 * time.Second
// CheckAvailable reports whether nmcli is installed and NetworkManager is
// reachable. Call it before using the rest of the package.
func CheckAvailable() error {
if _, err := exec.LookPath("nmcli"); err != nil {
return ErrNotFound
}
out, err := run("-t", "-f", "RUNNING", "general")
if err != nil {
return fmt.Errorf("%w: %v", ErrNotRunning, err)
}
if strings.TrimSpace(out) != "running" {
return ErrNotRunning
}
return nil
}
func run(args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, "nmcli", args...) // #nosec G204 -- args are fixed nmcli subcommands or user-supplied SSID/password, never shell-interpreted
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if errors.Is(err, exec.ErrNotFound) {
return "", ErrNotFound
}
msg := strings.TrimSpace(stderr.String())
if msg == "" {
msg = err.Error()
}
return "", fmt.Errorf("nmcli %s: %s", strings.Join(args, " "), msg)
}
return stdout.String(), nil
}
// getField runs `nmcli -g <field> ...` and returns the single trimmed value.
func getField(field string, args ...string) (string, error) {
out, err := run(append([]string{"-g", field}, args...)...)
if err != nil {
return "", err
}
return strings.TrimSpace(out), nil
}
// runTerse runs nmcli with -t -f <fields> and parses each output line into
// its colon-separated columns, unescaping nmcli's `\:` and `\\`.
func runTerse(fields string, args ...string) ([][]string, error) {
out, err := run(append([]string{"-t", "-f", fields}, args...)...)
if err != nil {
return nil, err
}
out = strings.TrimRight(out, "\n")
if out == "" {
return nil, nil
}
lines := strings.Split(out, "\n")
rows := make([][]string, 0, len(lines))
for _, line := range lines {
rows = append(rows, parseTerseLine(line))
}
return rows, nil
}
func parseTerseLine(line string) []string {
var fields []string
var cur strings.Builder
escaped := false
for _, r := range line {
if escaped {
cur.WriteRune(r)
escaped = false
continue
}
switch r {
case '\\':
escaped = true
case ':':
fields = append(fields, cur.String())
cur.Reset()
default:
cur.WriteRune(r)
}
}
fields = append(fields, cur.String())
return fields
}
// field is a small helper to safely index a parsed terse row.
func field(row []string, i int) string {
if i < 0 || i >= len(row) {
return ""
}
return row[i]
}
+44
View File
@@ -0,0 +1,44 @@
package network
// VPNConnection is a saved VPN or WireGuard connection profile.
type VPNConnection struct {
Name string
Type string // "vpn" or "wireguard"
Active bool
Device string
}
// ListVPN returns every saved connection whose type is "vpn" or "wireguard".
func ListVPN() ([]VPNConnection, error) {
rows, err := runTerse("NAME,TYPE,ACTIVE,DEVICE", "connection", "show")
if err != nil {
return nil, err
}
vpns := make([]VPNConnection, 0)
for _, row := range rows {
typ := field(row, 1)
if typ != "vpn" && typ != "wireguard" {
continue
}
vpns = append(vpns, VPNConnection{
Name: field(row, 0),
Type: typ,
Active: field(row, 2) == "yes",
Device: field(row, 3),
})
}
return vpns, nil
}
// VPNUp activates a VPN/WireGuard connection by name.
func VPNUp(name string) error {
_, err := run("connection", "up", "id", name)
return err
}
// VPNDown deactivates a VPN/WireGuard connection by name.
func VPNDown(name string) error {
_, err := run("connection", "down", "id", name)
return err
}
+118
View File
@@ -0,0 +1,118 @@
package network
import (
"errors"
"strconv"
)
var ErrNoWifiDevice = errors.New("no Wi-Fi device found")
// WifiDevice returns the first Wi-Fi radio device, if any.
func WifiDevice() (Device, error) {
devices, err := ListDevices()
if err != nil {
return Device{}, err
}
for _, d := range devices {
if d.Type == "wifi" {
return d, nil
}
}
return Device{}, ErrNoWifiDevice
}
// WifiNetwork is one access point seen by `nmcli device wifi list`.
type WifiNetwork struct {
Active bool // this is the AP we're currently associated with
SSID string
BSSID string
Signal int // 0-100
Security string
}
func (w WifiNetwork) Secured() bool {
return w.Security != "" && w.Security != "--"
}
// ListWifi returns visible access points, deduplicated by SSID (keeping the
// strongest signal seen for each network), sorted strongest-first.
func ListWifi() ([]WifiNetwork, error) {
rows, err := runTerse("IN-USE,SSID,BSSID,SIGNAL,SECURITY", "device", "wifi", "list")
if err != nil {
return nil, err
}
bySSID := make(map[string]WifiNetwork)
order := make([]string, 0, len(rows))
for _, row := range rows {
ssid := field(row, 1)
if ssid == "" {
continue // hidden network with no broadcast SSID
}
signal, _ := strconv.Atoi(field(row, 3))
net := WifiNetwork{
Active: field(row, 0) == "*",
SSID: ssid,
BSSID: field(row, 2),
Signal: signal,
Security: field(row, 4),
}
existing, ok := bySSID[ssid]
if !ok || net.Active || net.Signal > existing.Signal {
bySSID[ssid] = net
}
if !ok {
order = append(order, ssid)
}
}
networks := make([]WifiNetwork, 0, len(order))
for _, ssid := range order {
networks = append(networks, bySSID[ssid])
}
return networks, nil
}
// Rescan asks NetworkManager to trigger a fresh Wi-Fi scan. It returns once
// the scan is requested, not once it completes; call ListWifi shortly after.
func Rescan() error {
_, err := run("device", "wifi", "rescan")
return err
}
// ConnectWifi joins ssid, creating a new saved profile if none exists yet.
// Pass an empty password for open networks or when a saved profile already
// has one.
func ConnectWifi(ssid, password string) error {
args := []string{"device", "wifi", "connect", ssid}
if password != "" {
args = append(args, "password", password)
}
_, err := run(args...)
return err
}
// DisconnectWifi disconnects the given Wi-Fi device without forgetting it.
func DisconnectWifi(device string) error {
_, err := run("device", "disconnect", device)
return err
}
// SetWifiRadio turns the Wi-Fi radio on or off entirely.
func SetWifiRadio(on bool) error {
state := "off"
if on {
state = "on"
}
_, err := run("radio", "wifi", state)
return err
}
// RadioEnabled reports whether the Wi-Fi radio is currently switched on.
func RadioEnabled() (bool, error) {
out, err := getField("WIFI", "general")
if err != nil {
return false, err
}
return out == "enabled", nil
}