mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
freebsd: add initial support for running on FreeBSD, excluding Bluez,
ppd, and some things. Add a simple wpa_supplicant backend
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// sys/sys/backlight.h: brightness is a 0-100 percent;
|
||||
// BACKLIGHTGETSTATUS/BACKLIGHTUPDATESTATUS = _IOWR('G', 0/1, struct
|
||||
// backlight_props{uint32 brightness; uint32 nlevels; uint32 levels[100]}).
|
||||
const (
|
||||
backlightDevDir = "/dev/backlight"
|
||||
backlightGetStatus = 0xc1984700
|
||||
backlightUpdateStatus = 0xc1984701
|
||||
)
|
||||
|
||||
type backlightProps struct {
|
||||
brightness uint32
|
||||
nlevels uint32
|
||||
levels [100]uint32
|
||||
}
|
||||
|
||||
type BacklightBackend struct {
|
||||
devices syncmap.Map[string, string]
|
||||
}
|
||||
|
||||
func NewBacklightBackend() (*BacklightBackend, error) {
|
||||
b := &BacklightBackend{}
|
||||
if err := b.scanDevices(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func isGenericBacklightName(name string) bool {
|
||||
rest, ok := strings.CutPrefix(name, "backlight")
|
||||
if !ok || rest == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range rest {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *BacklightBackend) scanDevices() error {
|
||||
entries, err := os.ReadDir(backlightDevDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", backlightDevDir, err)
|
||||
}
|
||||
|
||||
// backlight_register (sys/dev/backlight/backlight.c) publishes each unit
|
||||
// as backlight/backlightN plus a driver-named alias for the same cdev;
|
||||
// dedupe on the device number and keep the descriptive alias.
|
||||
names := make(map[uint64]string)
|
||||
for _, entry := range entries {
|
||||
var st unix.Stat_t
|
||||
if err := unix.Stat(filepath.Join(backlightDevDir, entry.Name()), &st); err != nil {
|
||||
continue
|
||||
}
|
||||
rdev := uint64(st.Rdev)
|
||||
current, exists := names[rdev]
|
||||
if exists && !isGenericBacklightName(current) {
|
||||
continue
|
||||
}
|
||||
names[rdev] = entry.Name()
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
b.devices.Store("backlight:"+name, filepath.Join(backlightDevDir, name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func backlightIoctl(fd int, req uint, props *backlightProps) error {
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(unsafe.Pointer(props)))
|
||||
if errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readBrightness(path string) (int, error) {
|
||||
fd, err := unix.Open(path, unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
var props backlightProps
|
||||
if err := backlightIoctl(fd, backlightGetStatus, &props); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(props.brightness), nil
|
||||
}
|
||||
|
||||
func (b *BacklightBackend) Rescan() error {
|
||||
return b.scanDevices()
|
||||
}
|
||||
|
||||
func (b *BacklightBackend) GetDevices() ([]Device, error) {
|
||||
devices := make([]Device, 0)
|
||||
|
||||
b.devices.Range(func(id, path string) bool {
|
||||
brightness, err := readBrightness(path)
|
||||
if err != nil {
|
||||
log.Debugf("failed to read brightness for %s: %v", id, err)
|
||||
return true
|
||||
}
|
||||
|
||||
devices = append(devices, Device{
|
||||
Class: ClassBacklight,
|
||||
ID: id,
|
||||
Name: strings.TrimPrefix(id, "backlight:"),
|
||||
Current: brightness,
|
||||
Max: 100,
|
||||
CurrentPercent: brightness,
|
||||
Backend: "backlight",
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (b *BacklightBackend) SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error {
|
||||
if percent < 0 || percent > 100 {
|
||||
return fmt.Errorf("percent out of range: %d", percent)
|
||||
}
|
||||
|
||||
path, ok := b.devices.Load(id)
|
||||
if !ok {
|
||||
return fmt.Errorf("device not found: %s", id)
|
||||
}
|
||||
|
||||
value := percent
|
||||
switch {
|
||||
case percent == 0:
|
||||
value = 1
|
||||
case exponential:
|
||||
value = 1 + int(math.Round(math.Pow(float64(percent-1)/99.0, exponent)*99.0))
|
||||
}
|
||||
|
||||
fd, err := unix.Open(path, unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open %s: %w", path, err)
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
props := backlightProps{brightness: uint32(value)}
|
||||
if err := backlightIoctl(fd, backlightUpdateStatus, &props); err != nil {
|
||||
return fmt.Errorf("set brightness: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("set %s to %d%% (hw %d) via backlight(4)", id, percent, value)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
// devd(8) publishes device events on this SOCK_SEQPACKET socket, one
|
||||
// "!system=... subsystem=... type=..." record per packet.
|
||||
const devdSocketPath = "/var/run/devd.seqpacket.pipe"
|
||||
|
||||
const (
|
||||
devdMaxRetries = 5
|
||||
devdBaseDelay = 2 * time.Second
|
||||
devdMaxDelay = 60 * time.Second
|
||||
)
|
||||
|
||||
type DevdMonitor struct {
|
||||
stop chan struct{}
|
||||
rescanMutex sync.Mutex
|
||||
rescanTimer *time.Timer
|
||||
rescanPending bool
|
||||
}
|
||||
|
||||
func newDevdMonitor(manager *Manager) *DevdMonitor {
|
||||
m := &DevdMonitor{
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
go m.run(manager)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) run(manager *Manager) {
|
||||
failures := 0
|
||||
for {
|
||||
if err := m.monitorLoop(manager); err != nil {
|
||||
log.Errorf("Devd monitor error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
failures++
|
||||
if failures > devdMaxRetries {
|
||||
log.Errorf("Devd monitor exceeded %d retries, giving up", devdMaxRetries)
|
||||
return
|
||||
}
|
||||
|
||||
delay := min(devdBaseDelay*time.Duration(1<<(failures-1)), devdMaxDelay)
|
||||
log.Infof("Devd monitor reconnecting in %v (attempt %d/%d)", delay, failures, devdMaxRetries)
|
||||
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) monitorLoop(manager *Manager) error {
|
||||
conn, err := net.Dial("unixpacket", devdSocketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-m.stop:
|
||||
conn.Close()
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("Devd monitor started for backlight/drm events")
|
||||
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-m.stop:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.handleEvent(manager, string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) handleEvent(manager *Manager, event string) {
|
||||
notification, ok := strings.CutPrefix(event, "!")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fields := parseDevdEvent(notification)
|
||||
switch fields["system"] {
|
||||
case "DRM":
|
||||
m.debouncedRescan(manager)
|
||||
case "DEVFS":
|
||||
if fields["subsystem"] != "CDEV" {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(fields["cdev"], "backlight/") {
|
||||
return
|
||||
}
|
||||
m.debouncedRescan(manager)
|
||||
}
|
||||
}
|
||||
|
||||
func parseDevdEvent(s string) map[string]string {
|
||||
fields := make(map[string]string)
|
||||
for _, part := range strings.Fields(s) {
|
||||
k, v, ok := strings.Cut(part, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fields[k] = v
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) debouncedRescan(manager *Manager) {
|
||||
m.rescanMutex.Lock()
|
||||
defer m.rescanMutex.Unlock()
|
||||
|
||||
m.rescanPending = true
|
||||
|
||||
if m.rescanTimer != nil {
|
||||
m.rescanTimer.Reset(2 * time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
|
||||
m.rescanMutex.Lock()
|
||||
pending := m.rescanPending
|
||||
m.rescanPending = false
|
||||
m.rescanMutex.Unlock()
|
||||
|
||||
if !pending {
|
||||
return
|
||||
}
|
||||
|
||||
manager.Rescan()
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) Close() {
|
||||
close(m.stop)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func NewManagerWithOptions(exponential bool) (*Manager, error) {
|
||||
}
|
||||
|
||||
go m.initLogind()
|
||||
go m.initSysfs()
|
||||
go m.initNative()
|
||||
go m.initDDC()
|
||||
|
||||
return m, nil
|
||||
@@ -40,39 +40,6 @@ func (m *Manager) initLogind() {
|
||||
log.Info("Logind backend initialized - will use for brightness control")
|
||||
}
|
||||
|
||||
func (m *Manager) initSysfs() {
|
||||
log.Debug("Initializing sysfs backend...")
|
||||
sysfs, err := NewSysfsBackend()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize sysfs backend: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := sysfs.GetDevices()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get initial sysfs devices: %v", err)
|
||||
m.sysfsBackend = sysfs
|
||||
m.sysfsReady = true
|
||||
m.updateState()
|
||||
m.initUdev()
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Sysfs backend initialized with %d devices", len(devices))
|
||||
for _, d := range devices {
|
||||
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
|
||||
}
|
||||
|
||||
m.sysfsBackend = sysfs
|
||||
m.sysfsReady = true
|
||||
m.updateState()
|
||||
m.initUdev()
|
||||
}
|
||||
|
||||
func (m *Manager) initUdev() {
|
||||
m.udevMonitor = NewUdevMonitor(m)
|
||||
}
|
||||
|
||||
func (m *Manager) initDDC() {
|
||||
ddc, err := NewDDCBackend()
|
||||
if err != nil {
|
||||
@@ -96,9 +63,9 @@ func (m *Manager) Rescan() {
|
||||
}
|
||||
}
|
||||
|
||||
if m.sysfsReady && m.sysfsBackend != nil {
|
||||
if err := m.sysfsBackend.Rescan(); err != nil {
|
||||
log.Debugf("Sysfs rescan failed: %v", err)
|
||||
if m.nativeReady && m.nativeBackend != nil {
|
||||
if err := m.nativeBackend.Rescan(); err != nil {
|
||||
log.Debugf("Native backend rescan failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,10 +117,10 @@ func stateChanged(old, new State) bool {
|
||||
func (m *Manager) updateState() {
|
||||
allDevices := make([]Device, 0)
|
||||
|
||||
if m.sysfsReady && m.sysfsBackend != nil {
|
||||
devices, err := m.sysfsBackend.GetDevices()
|
||||
if m.nativeReady && m.nativeBackend != nil {
|
||||
devices, err := m.nativeBackend.GetDevices()
|
||||
if err != nil {
|
||||
log.Debugf("Failed to get sysfs devices: %v", err)
|
||||
log.Debugf("Failed to get native backend devices: %v", err)
|
||||
}
|
||||
if err == nil {
|
||||
allDevices = append(allDevices, devices...)
|
||||
@@ -232,18 +199,21 @@ func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, expone
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
var err error
|
||||
if deviceClass == ClassDDC {
|
||||
switch {
|
||||
case deviceClass == ClassDDC:
|
||||
log.Debugf("Calling DDC backend for %s", deviceID)
|
||||
err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() {
|
||||
m.updateState()
|
||||
m.debouncedBroadcast(deviceID)
|
||||
})
|
||||
} else if m.logindReady && m.logindBackend != nil {
|
||||
case m.logindReady && m.logindBackend != nil:
|
||||
log.Debugf("Calling logind backend for %s", deviceID)
|
||||
err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent)
|
||||
} else {
|
||||
log.Debugf("Calling sysfs backend for %s", deviceID)
|
||||
err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
|
||||
case m.nativeBackend != nil:
|
||||
log.Debugf("Calling native backend for %s", deviceID)
|
||||
err = m.nativeBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
|
||||
default:
|
||||
err = fmt.Errorf("no brightness backend for %s", deviceID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
func (m *Manager) initNative() {
|
||||
log.Debug("Initializing backlight backend...")
|
||||
backend, err := NewBacklightBackend()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize backlight backend: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := backend.GetDevices()
|
||||
if err == nil {
|
||||
log.Infof("Backlight backend initialized with %d devices", len(devices))
|
||||
}
|
||||
|
||||
m.nativeBackend = backend
|
||||
m.nativeReady = true
|
||||
m.updateState()
|
||||
m.monitor = newDevdMonitor(m)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
func (m *Manager) initNative() {
|
||||
log.Debug("Initializing sysfs backend...")
|
||||
sysfs, err := NewSysfsBackend()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize sysfs backend: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := sysfs.GetDevices()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get initial sysfs devices: %v", err)
|
||||
m.sysfsBackend = sysfs
|
||||
m.nativeBackend = sysfs
|
||||
m.nativeReady = true
|
||||
m.updateState()
|
||||
m.monitor = NewUdevMonitor(m)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Sysfs backend initialized with %d devices", len(devices))
|
||||
for _, d := range devices {
|
||||
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
|
||||
}
|
||||
|
||||
m.sysfsBackend = sysfs
|
||||
m.nativeBackend = sysfs
|
||||
m.nativeReady = true
|
||||
m.updateState()
|
||||
m.monitor = NewUdevMonitor(m)
|
||||
}
|
||||
+5
-4
@@ -43,7 +43,7 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -180,8 +180,9 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
|
||||
m := &Manager{
|
||||
logindBackend: nil,
|
||||
sysfsBackend: sysfs,
|
||||
nativeBackend: sysfs,
|
||||
logindReady: false,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -243,7 +244,7 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -33,14 +33,25 @@ type DeviceUpdate struct {
|
||||
Device Device `json:"device"`
|
||||
}
|
||||
|
||||
type Backend interface {
|
||||
Rescan() error
|
||||
GetDevices() ([]Device, error)
|
||||
SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error
|
||||
}
|
||||
|
||||
type deviceMonitor interface {
|
||||
Close()
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
logindBackend *LogindBackend
|
||||
sysfsBackend *SysfsBackend
|
||||
nativeBackend Backend
|
||||
ddcBackend *DDCBackend
|
||||
udevMonitor *UdevMonitor
|
||||
monitor deviceMonitor
|
||||
|
||||
logindReady bool
|
||||
sysfsReady bool
|
||||
nativeReady bool
|
||||
ddcReady bool
|
||||
|
||||
exponential bool
|
||||
@@ -170,8 +181,8 @@ func (m *Manager) Close() {
|
||||
return true
|
||||
})
|
||||
|
||||
if m.udevMonitor != nil {
|
||||
m.udevMonitor.Close()
|
||||
if m.monitor != nil {
|
||||
m.monitor.Close()
|
||||
}
|
||||
|
||||
if m.logindBackend != nil {
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ func setupTestManager(t *testing.T) (*Manager, string) {
|
||||
|
||||
m := &Manager{
|
||||
sysfsBackend: sysfs,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
// Default ctrl_interface directory on FreeBSD and minimal Linux setups; see
|
||||
// https://man.freebsd.org/cgi/man.cgi?query=wpa_supplicant.conf&sektion=5
|
||||
const wpaSupplicantCtrlDir = "/var/run/wpa_supplicant"
|
||||
|
||||
type wpaConnectAttempt struct {
|
||||
ssid string
|
||||
netID int
|
||||
saved bool
|
||||
newlyAdded bool
|
||||
prompted bool
|
||||
sawTempDisabled bool
|
||||
finalized bool
|
||||
deadline time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type WpaSupplicantBackend struct {
|
||||
ctrlDir string
|
||||
ifname string
|
||||
cmd *wpaCtrlConn
|
||||
monitor *wpaCtrlConn
|
||||
state *BackendState
|
||||
stateMutex sync.RWMutex
|
||||
promptBroker PromptBroker
|
||||
onStateChange func()
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
curAttempt *wpaConnectAttempt
|
||||
attemptMutex sync.RWMutex
|
||||
|
||||
savedIDs map[string]int
|
||||
savedIDsMu sync.Mutex
|
||||
|
||||
recentScans map[string]time.Time
|
||||
recentScansMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewWpaSupplicantBackend() (*WpaSupplicantBackend, error) {
|
||||
return &WpaSupplicantBackend{
|
||||
ctrlDir: wpaSupplicantCtrlDir,
|
||||
state: &BackendState{
|
||||
Backend: "wpa_supplicant",
|
||||
WiFiNetworks: []WiFiNetwork{},
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
savedIDs: make(map[string]int),
|
||||
recentScans: make(map[string]time.Time),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func discoverWpaInterfaces(ctrlDir string) ([]string, error) {
|
||||
entries, err := os.ReadDir(ctrlDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", ctrlDir, err)
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
// wpa_supplicant creates an extra p2p-dev-<ifname> management socket
|
||||
// when built with CONFIG_P2P; it is not a station interface.
|
||||
if strings.HasPrefix(entry.Name(), "p2p-dev-") {
|
||||
continue
|
||||
}
|
||||
names = append(names, entry.Name())
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) Initialize() error {
|
||||
names, err := discoverWpaInterfaces(b.ctrlDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to discover wpa_supplicant interfaces: %w", err)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return fmt.Errorf("no wpa_supplicant control sockets in %s", b.ctrlDir)
|
||||
}
|
||||
|
||||
b.ifname = names[0]
|
||||
if len(names) > 1 {
|
||||
log.Infof("wpa_supplicant manages %d interfaces; using %s", len(names), b.ifname)
|
||||
}
|
||||
|
||||
conn, err := newWpaCtrlConn(filepath.Join(b.ctrlDir, b.ifname))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open wpa_ctrl socket: %w", err)
|
||||
}
|
||||
b.cmd = conn
|
||||
|
||||
reply, err := b.cmd.request("PING")
|
||||
if err != nil {
|
||||
b.cmd.close()
|
||||
return fmt.Errorf("wpa_supplicant not responding on %s: %w", b.ifname, err)
|
||||
}
|
||||
if reply != "PONG" {
|
||||
b.cmd.close()
|
||||
return fmt.Errorf("unexpected PING reply from wpa_supplicant: %s", reply)
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiDevice = b.ifname
|
||||
b.state.WiFiEnabled = true
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||
log.Warnf("Failed to get initial saved WiFi networks: %v", err)
|
||||
}
|
||||
|
||||
if err := b.updateState(); err != nil {
|
||||
b.cmd.close()
|
||||
return fmt.Errorf("failed to get initial state: %w", err)
|
||||
}
|
||||
|
||||
if _, err := b.updateWiFiNetworks(); err != nil {
|
||||
log.Warnf("Failed to get initial WiFi networks: %v", err)
|
||||
}
|
||||
|
||||
if err := b.ScanWiFi(); err != nil {
|
||||
log.Debugf("Initial WiFi scan not started: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) Close() {
|
||||
b.StopMonitoring()
|
||||
|
||||
if b.monitor != nil {
|
||||
_ = b.monitor.send("DETACH")
|
||||
b.monitor.close()
|
||||
}
|
||||
if b.cmd != nil {
|
||||
b.cmd.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetCurrentState() (*BackendState, error) {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
|
||||
state := *b.state
|
||||
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
|
||||
state.SavedWiFiNetworks = append([]WiFiNetwork(nil), b.state.SavedWiFiNetworks...)
|
||||
state.EthernetDevices = append([]EthernetDevice(nil), b.state.EthernetDevices...)
|
||||
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
|
||||
state.WiFiDevices = b.getWiFiDevicesLocked()
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetPromptBroker() PromptBroker {
|
||||
return b.promptBroker
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) SetPromptBroker(broker PromptBroker) error {
|
||||
if broker == nil {
|
||||
return fmt.Errorf("broker cannot be nil")
|
||||
}
|
||||
|
||||
b.promptBroker = broker
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error {
|
||||
if b.promptBroker == nil {
|
||||
return fmt.Errorf("prompt broker not initialized")
|
||||
}
|
||||
|
||||
return b.promptBroker.Resolve(token, PromptReply{
|
||||
Secrets: secrets,
|
||||
Save: save,
|
||||
Cancel: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) CancelCredentials(token string) error {
|
||||
if b.promptBroker == nil {
|
||||
return fmt.Errorf("prompt broker not initialized")
|
||||
}
|
||||
|
||||
return b.promptBroker.Resolve(token, PromptReply{
|
||||
Cancel: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) StopMonitoring() {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
default:
|
||||
close(b.stopChan)
|
||||
}
|
||||
b.sigWG.Wait()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) promptForPSK(ssid, reason string) (string, bool) {
|
||||
if b.promptBroker == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
token, err := b.promptBroker.Ask(ctx, PromptRequest{
|
||||
SSID: ssid,
|
||||
SettingName: "802-11-wireless-security",
|
||||
Fields: []string{"psk"},
|
||||
Reason: reason,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("failed to request credentials for %s: %v", ssid, err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
reply, err := b.promptBroker.Wait(ctx, token)
|
||||
if err != nil || reply.Cancel {
|
||||
return "", false
|
||||
}
|
||||
|
||||
psk, ok := reply.Secrets["psk"]
|
||||
if !ok || psk == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return psk, true
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) storeSavedIDs(ids map[string]int) {
|
||||
b.savedIDsMu.Lock()
|
||||
b.savedIDs = ids
|
||||
b.savedIDsMu.Unlock()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) savedNetworkID(ssid string) (int, bool) {
|
||||
b.savedIDsMu.Lock()
|
||||
defer b.savedIDsMu.Unlock()
|
||||
id, ok := b.savedIDs[ssid]
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) seenInRecentScan(ssid string) bool {
|
||||
b.recentScansMu.Lock()
|
||||
defer b.recentScansMu.Unlock()
|
||||
lastSeen, ok := b.recentScans[ssid]
|
||||
return ok && time.Since(lastSeen) < 30*time.Second
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) setConnectError(code string) {
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = code
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
wpaCtrlRequestTimeout = 5 * time.Second
|
||||
wpaCtrlReadBufferSize = 65536
|
||||
)
|
||||
|
||||
// Client side of the wpa_ctrl protocol: a unix datagram socket bound to a
|
||||
// local path, connected to wpa_supplicant's per-interface socket. See
|
||||
// https://w1.fi/wpa_supplicant/devel/ctrl_iface_page.html
|
||||
type wpaCtrlConn struct {
|
||||
sockPath string
|
||||
localDir string
|
||||
mu sync.Mutex
|
||||
conn *net.UnixConn
|
||||
seq int
|
||||
}
|
||||
|
||||
func wpaCtrlLocalSocketBase() string {
|
||||
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
func newWpaCtrlConn(sockPath string) (*wpaCtrlConn, error) {
|
||||
localDir, err := os.MkdirTemp(wpaCtrlLocalSocketBase(), "dms-wpa-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create wpa_ctrl socket dir: %w", err)
|
||||
}
|
||||
|
||||
c := &wpaCtrlConn{sockPath: sockPath, localDir: localDir}
|
||||
if err := c.dialLocked(); err != nil {
|
||||
os.RemoveAll(localDir)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) dialLocked() error {
|
||||
c.seq++
|
||||
localPath := filepath.Join(c.localDir, fmt.Sprintf("ctrl_%d", c.seq))
|
||||
os.Remove(localPath)
|
||||
|
||||
conn, err := net.DialUnix("unixgram",
|
||||
&net.UnixAddr{Name: localPath, Net: "unixgram"},
|
||||
&net.UnixAddr{Name: c.sockPath, Net: "unixgram"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial %s: %w", c.sockPath, err)
|
||||
}
|
||||
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
c.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) reconnectLocked() error {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
return c.dialLocked()
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) reconnect() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.reconnectLocked()
|
||||
}
|
||||
|
||||
// request sends a command and waits for its reply, retrying once on a fresh
|
||||
// socket so a wpa_supplicant restart between requests is transparent.
|
||||
func (c *wpaCtrlConn) request(cmd string) (string, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
reply, err := c.roundTripLocked(cmd)
|
||||
if err == nil {
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
if rerr := c.reconnectLocked(); rerr != nil {
|
||||
return "", err
|
||||
}
|
||||
return c.roundTripLocked(cmd)
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) roundTripLocked(cmd string) (string, error) {
|
||||
if c.conn == nil {
|
||||
if err := c.dialLocked(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
_ = c.conn.SetDeadline(time.Now().Add(wpaCtrlRequestTimeout))
|
||||
if _, err := c.conn.Write([]byte(cmd)); err != nil {
|
||||
return "", fmt.Errorf("write %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
buf := make([]byte, wpaCtrlReadBufferSize)
|
||||
for {
|
||||
n, err := c.conn.Read(buf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read reply for %q: %w", cmd, err)
|
||||
}
|
||||
// Unsolicited event datagrams carry a "<priority>" prefix, command
|
||||
// replies never do (ctrl_iface_page.html, "Control interface data").
|
||||
if n > 0 && buf[0] == '<' {
|
||||
continue
|
||||
}
|
||||
return strings.TrimRight(string(buf[:n]), "\n"), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) send(cmd string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn == nil {
|
||||
if err := c.dialLocked(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(wpaCtrlRequestTimeout))
|
||||
if _, err := c.conn.Write([]byte(cmd)); err != nil {
|
||||
return fmt.Errorf("write %q: %w", cmd, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) readDatagram(timeout time.Duration) (string, error) {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
return "", fmt.Errorf("wpa_ctrl socket closed")
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
buf := make([]byte, wpaCtrlReadBufferSize)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimRight(string(buf[:n]), "\n"), nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) attach() error {
|
||||
reply, err := c.request("ATTACH")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reply != "OK" {
|
||||
return fmt.Errorf("ATTACH failed: %s", reply)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) close() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
os.RemoveAll(c.localDir)
|
||||
}
|
||||
|
||||
func isWpaCtrlTimeout(err error) bool {
|
||||
netErr, ok := err.(net.Error)
|
||||
return ok && netErr.Timeout()
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func interfaceIPv4(ifname string) string {
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
|
||||
return ipv4.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func looksWirelessName(name string) bool {
|
||||
return strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) isWpaManaged(name string) bool {
|
||||
return name == b.ifname
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ethernetDevices() []EthernetDevice {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var devices []EthernetDevice
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if looksVirtual(iface.Name) || looksWirelessName(iface.Name) {
|
||||
continue
|
||||
}
|
||||
if b.isWpaManaged(iface.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
up := iface.Flags&net.FlagUp != 0
|
||||
running := iface.Flags&net.FlagRunning != 0
|
||||
ip := interfaceIPv4(iface.Name)
|
||||
|
||||
stateStr := "off"
|
||||
switch {
|
||||
case running && ip != "":
|
||||
stateStr = "routable"
|
||||
case running:
|
||||
stateStr = "carrier"
|
||||
case up:
|
||||
stateStr = "no-carrier"
|
||||
}
|
||||
|
||||
devices = append(devices, EthernetDevice{
|
||||
Name: iface.Name,
|
||||
HwAddress: iface.HardwareAddr.String(),
|
||||
State: stateStr,
|
||||
Connected: up && running,
|
||||
IP: ip,
|
||||
})
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
func wiredConnectionsFromEthernetDevices(devices []EthernetDevice) []WiredConnection {
|
||||
conns := make([]WiredConnection, 0, len(devices))
|
||||
for _, dev := range devices {
|
||||
conns = append(conns, WiredConnection{
|
||||
Path: dbus.ObjectPath("/" + dev.Name),
|
||||
ID: dev.Name,
|
||||
UUID: "wired:" + dev.Name,
|
||||
Type: "ethernet",
|
||||
IsActive: dev.Connected,
|
||||
})
|
||||
}
|
||||
return conns
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetEthernetDevices() []EthernetDevice {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
return append([]EthernetDevice(nil), b.state.EthernetDevices...)
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
return wiredConnectionsFromEthernetDevices(b.ethernetDevices()), nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetWiredNetworkDetails(id string) (*WiredNetworkInfoResponse, error) {
|
||||
ifname := strings.TrimPrefix(id, "wired:")
|
||||
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get interface: %w", err)
|
||||
}
|
||||
|
||||
addrs, _ := iface.Addrs()
|
||||
var ipv4s, ipv6s []string
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
|
||||
ipv4s = append(ipv4s, ipnet.String())
|
||||
} else if ipv6 := ipnet.IP.To16(); ipv6 != nil {
|
||||
ipv6s = append(ipv6s, ipnet.String())
|
||||
}
|
||||
}
|
||||
|
||||
return &WiredNetworkInfoResponse{
|
||||
UUID: id,
|
||||
IFace: ifname,
|
||||
HwAddr: iface.HardwareAddr.String(),
|
||||
IPv4: WiredIPConfig{
|
||||
IPs: ipv4s,
|
||||
},
|
||||
IPv6: WiredIPConfig{
|
||||
IPs: ipv6s,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
// Event strings per WPA_EVENT_* in contrib/wpa/src/common/wpa_ctrl.h
|
||||
// (freebsd-src).
|
||||
const (
|
||||
wpaEventConnected = "CTRL-EVENT-CONNECTED"
|
||||
wpaEventDisconnected = "CTRL-EVENT-DISCONNECTED"
|
||||
wpaEventScanResults = "CTRL-EVENT-SCAN-RESULTS"
|
||||
wpaEventTempDisabled = "CTRL-EVENT-SSID-TEMP-DISABLED"
|
||||
)
|
||||
|
||||
const (
|
||||
wpaMonitorReadTimeout = 5 * time.Second
|
||||
wpaMonitorPingInterval = 30 * time.Second
|
||||
wpaMonitorReconnectDelay = 2 * time.Second
|
||||
)
|
||||
|
||||
type wpaEvent struct {
|
||||
priority int
|
||||
name string
|
||||
args string
|
||||
}
|
||||
|
||||
func parseWpaEventLine(line string) (wpaEvent, bool) {
|
||||
if len(line) < 3 || line[0] != '<' {
|
||||
return wpaEvent{}, false
|
||||
}
|
||||
|
||||
end := strings.IndexByte(line, '>')
|
||||
if end < 0 {
|
||||
return wpaEvent{}, false
|
||||
}
|
||||
|
||||
priority, err := strconv.Atoi(line[1:end])
|
||||
if err != nil {
|
||||
return wpaEvent{}, false
|
||||
}
|
||||
|
||||
rest := line[end+1:]
|
||||
name, args, _ := strings.Cut(rest, " ")
|
||||
if name == "" {
|
||||
return wpaEvent{}, false
|
||||
}
|
||||
|
||||
return wpaEvent{priority: priority, name: name, args: args}, true
|
||||
}
|
||||
|
||||
// Args format: id=%d ssid="%s" auth_failures=%u duration=%d reason=%s, per
|
||||
// wpas_auth_failed in contrib/wpa/wpa_supplicant/wpa_supplicant.c
|
||||
// (freebsd-src); the SSID is printf_encoded inside the quotes.
|
||||
func parseWpaTempDisabled(args string) (ssid string, reason string) {
|
||||
if start := strings.Index(args, `ssid="`); start >= 0 {
|
||||
raw := args[start+len(`ssid="`):]
|
||||
if end := indexUnescapedQuote(raw); end >= 0 {
|
||||
ssid = decodeWpaSSIDText(raw[:end])
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range strings.Fields(args) {
|
||||
if value, ok := strings.CutPrefix(field, "reason="); ok {
|
||||
reason = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ssid, reason
|
||||
}
|
||||
|
||||
func indexUnescapedQuote(s string) int {
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\\':
|
||||
i++
|
||||
case '"':
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) StartMonitoring(onStateChange func()) error {
|
||||
b.onStateChange = onStateChange
|
||||
|
||||
monitor, err := newWpaCtrlConn(filepath.Join(b.ctrlDir, b.ifname))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open wpa_ctrl monitor socket: %w", err)
|
||||
}
|
||||
if err := monitor.attach(); err != nil {
|
||||
monitor.close()
|
||||
return fmt.Errorf("failed to attach wpa_ctrl monitor: %w", err)
|
||||
}
|
||||
b.monitor = monitor
|
||||
|
||||
b.sigWG.Add(1)
|
||||
go b.monitorLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) monitorLoop() {
|
||||
defer b.sigWG.Done()
|
||||
|
||||
lastActivity := time.Now()
|
||||
pingOutstanding := false
|
||||
var pingSent time.Time
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
msg, err := b.monitor.readDatagram(wpaMonitorReadTimeout)
|
||||
switch {
|
||||
case err == nil:
|
||||
lastActivity = time.Now()
|
||||
if strings.HasPrefix(msg, "<") {
|
||||
b.handleEvent(msg)
|
||||
continue
|
||||
}
|
||||
if msg == "PONG" {
|
||||
pingOutstanding = false
|
||||
}
|
||||
|
||||
case isWpaCtrlTimeout(err):
|
||||
if pingOutstanding && time.Since(pingSent) > wpaCtrlRequestTimeout {
|
||||
b.reattachMonitor()
|
||||
pingOutstanding = false
|
||||
lastActivity = time.Now()
|
||||
continue
|
||||
}
|
||||
if !pingOutstanding && time.Since(lastActivity) > wpaMonitorPingInterval {
|
||||
if b.monitor.send("PING") != nil {
|
||||
b.reattachMonitor()
|
||||
lastActivity = time.Now()
|
||||
continue
|
||||
}
|
||||
pingOutstanding = true
|
||||
pingSent = time.Now()
|
||||
}
|
||||
|
||||
default:
|
||||
b.reattachMonitor()
|
||||
pingOutstanding = false
|
||||
lastActivity = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) reattachMonitor() {
|
||||
for {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if err := b.monitor.reconnect(); err == nil {
|
||||
if err := b.monitor.attach(); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
case <-time.After(wpaMonitorReconnectDelay):
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("wpa_supplicant monitor reattached on %s", b.ifname)
|
||||
|
||||
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||
log.Warnf("failed to refresh saved networks after wpa reattach: %v", err)
|
||||
}
|
||||
if err := b.updateState(); err != nil {
|
||||
log.Warnf("failed to refresh state after wpa reattach: %v", err)
|
||||
}
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleEvent(raw string) {
|
||||
event, ok := parseWpaEventLine(raw)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch event.name {
|
||||
case wpaEventScanResults:
|
||||
b.handleScanResults()
|
||||
case wpaEventConnected:
|
||||
b.handleConnected()
|
||||
case wpaEventDisconnected:
|
||||
b.handleDisconnected()
|
||||
case wpaEventTempDisabled:
|
||||
b.handleTempDisabled(event.args)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleScanResults() {
|
||||
if _, err := b.updateWiFiNetworks(); err != nil {
|
||||
log.Warnf("failed to update WiFi networks after scan: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleConnected() {
|
||||
if err := b.updateState(); err != nil {
|
||||
log.Warnf("failed to update wpa state after connect event: %v", err)
|
||||
}
|
||||
|
||||
b.attemptMutex.RLock()
|
||||
att := b.curAttempt
|
||||
b.attemptMutex.RUnlock()
|
||||
|
||||
b.stateMutex.RLock()
|
||||
currentSSID := b.state.WiFiSSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
if att != nil && att.ssid == currentSSID {
|
||||
b.finalizeAttempt(att, "")
|
||||
b.attemptMutex.Lock()
|
||||
if b.curAttempt == att {
|
||||
b.curAttempt = nil
|
||||
}
|
||||
b.attemptMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||
log.Warnf("failed to refresh saved networks after connect event: %v", err)
|
||||
}
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleDisconnected() {
|
||||
if err := b.updateState(); err != nil {
|
||||
log.Warnf("failed to update wpa state after disconnect event: %v", err)
|
||||
}
|
||||
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||
log.Warnf("failed to refresh saved networks after disconnect event: %v", err)
|
||||
}
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleTempDisabled(args string) {
|
||||
ssid, reason := parseWpaTempDisabled(args)
|
||||
|
||||
b.attemptMutex.RLock()
|
||||
att := b.curAttempt
|
||||
b.attemptMutex.RUnlock()
|
||||
|
||||
if att == nil || att.ssid != ssid {
|
||||
return
|
||||
}
|
||||
|
||||
att.mu.Lock()
|
||||
att.sawTempDisabled = true
|
||||
att.mu.Unlock()
|
||||
|
||||
code := errdefs.ErrConnectionFailed
|
||||
// WRONG_KEY is the reason wpas_auth_failed reports for a PSK mismatch
|
||||
// (could_be_psk_mismatch path in contrib/wpa/wpa_supplicant/events.c).
|
||||
if reason == "WRONG_KEY" {
|
||||
code = errdefs.ErrBadCredentials
|
||||
}
|
||||
|
||||
b.finalizeAttempt(att, code)
|
||||
|
||||
b.attemptMutex.Lock()
|
||||
if b.curAttempt == att {
|
||||
b.curAttempt = nil
|
||||
}
|
||||
b.attemptMutex.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"net"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseWpaScanResults(t *testing.T) {
|
||||
text := "bssid / frequency / signal level / flags / ssid\n" +
|
||||
"02:00:01:02:03:04\t2412\t-45\t[WPA2-PSK-CCMP][ESS]\tHome\n" +
|
||||
"02:00:01:02:03:05\t5180\t-60\t[WPA2-EAP-CCMP][ESS]\tOffice\n" +
|
||||
"02:00:01:02:03:06\t2437\t-70\t[ESS]\tCafe\n" +
|
||||
"02:00:01:02:03:07\t2462\t-80\t[WPA2-PSK-CCMP][ESS]\t\n" +
|
||||
"02:00:01:02:03:08\t2412\t-50\t[WEP][ESS]\tTab\\tNet\n" +
|
||||
"garbage line without tabs\n"
|
||||
|
||||
results := parseWpaScanResults(text)
|
||||
|
||||
assert.Len(t, results, 5)
|
||||
assert.Equal(t, "02:00:01:02:03:04", results[0].BSSID)
|
||||
assert.Equal(t, uint32(2412), results[0].Frequency)
|
||||
assert.Equal(t, -45, results[0].Level)
|
||||
assert.Equal(t, "[WPA2-PSK-CCMP][ESS]", results[0].Flags)
|
||||
assert.Equal(t, "Home", results[0].SSID)
|
||||
assert.Empty(t, results[3].SSID)
|
||||
assert.Equal(t, "Tab\tNet", results[4].SSID)
|
||||
}
|
||||
|
||||
func TestWpaWiFiNetworksFromScanResults(t *testing.T) {
|
||||
results := []wpaScanResult{
|
||||
{BSSID: "02:00:01:02:03:04", Frequency: 2412, Level: -45, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: "Home"},
|
||||
{BSSID: "02:00:01:02:03:05", Frequency: 5180, Level: -80, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: "Home"},
|
||||
{BSSID: "02:00:01:02:03:06", Frequency: 5200, Level: -60, Flags: "[WPA2-EAP-CCMP][ESS]", SSID: "Office"},
|
||||
{BSSID: "02:00:01:02:03:07", Frequency: 2437, Level: -70, Flags: "[ESS]", SSID: "Cafe"},
|
||||
{BSSID: "02:00:01:02:03:08", Frequency: 2462, Level: -50, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: ""},
|
||||
}
|
||||
|
||||
networks := wpaWiFiNetworksFromScanResults(results)
|
||||
|
||||
byNet := make(map[string]WiFiNetwork, len(networks))
|
||||
for _, network := range networks {
|
||||
byNet[network.SSID] = network
|
||||
}
|
||||
|
||||
assert.Len(t, networks, 3)
|
||||
|
||||
home := byNet["Home"]
|
||||
assert.Equal(t, "02:00:01:02:03:04", home.BSSID, "strongest BSS should win")
|
||||
assert.Equal(t, uint8(55), home.Signal)
|
||||
assert.True(t, home.Secured)
|
||||
assert.False(t, home.Enterprise)
|
||||
assert.Equal(t, uint32(1), home.Channel)
|
||||
|
||||
office := byNet["Office"]
|
||||
assert.True(t, office.Secured)
|
||||
assert.True(t, office.Enterprise)
|
||||
|
||||
cafe := byNet["Cafe"]
|
||||
assert.False(t, cafe.Secured)
|
||||
assert.Equal(t, "infrastructure", cafe.Mode)
|
||||
}
|
||||
|
||||
func TestSignalPercentFromDbm(t *testing.T) {
|
||||
testCases := []struct {
|
||||
dbm int
|
||||
expected uint8
|
||||
}{
|
||||
{-30, 70},
|
||||
{-100, 0},
|
||||
{-110, 0},
|
||||
{0, 100},
|
||||
{10, 100},
|
||||
{-55, 45},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t, tc.expected, signalPercentFromDbm(tc.dbm), "dbm=%d", tc.dbm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWpaStatus(t *testing.T) {
|
||||
text := "bssid=02:00:01:02:03:04\n" +
|
||||
"freq=2412\n" +
|
||||
"ssid=Home Network\n" +
|
||||
"id=3\n" +
|
||||
"key_mgmt=WPA2-PSK\n" +
|
||||
"wpa_state=COMPLETED\n" +
|
||||
"ip_address=192.168.1.21\n"
|
||||
|
||||
st := parseWpaStatus(text)
|
||||
|
||||
assert.Equal(t, "COMPLETED", st.wpaState)
|
||||
assert.Equal(t, "Home Network", st.ssid)
|
||||
assert.Equal(t, "02:00:01:02:03:04", st.bssid)
|
||||
assert.Equal(t, "192.168.1.21", st.ipAddress)
|
||||
assert.Equal(t, "WPA2-PSK", st.keyMgmt)
|
||||
assert.Equal(t, 3, st.networkID)
|
||||
}
|
||||
|
||||
func TestParseWpaStatus_Disconnected(t *testing.T) {
|
||||
st := parseWpaStatus("wpa_state=DISCONNECTED\naddress=02:00:01:02:03:04\n")
|
||||
|
||||
assert.Equal(t, "DISCONNECTED", st.wpaState)
|
||||
assert.Empty(t, st.ssid)
|
||||
assert.Equal(t, -1, st.networkID)
|
||||
}
|
||||
|
||||
func TestParseWpaSignalPollRSSI(t *testing.T) {
|
||||
rssi, ok := parseWpaSignalPollRSSI("RSSI=-62\nLINKSPEED=433\nNOISE=9999\nFREQUENCY=5180\n")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, -62, rssi)
|
||||
|
||||
_, ok = parseWpaSignalPollRSSI("FAIL")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestParseWpaListNetworks(t *testing.T) {
|
||||
text := "network id / ssid / bssid / flags\n" +
|
||||
"0\tHome\tany\t[CURRENT]\n" +
|
||||
"1\tOffice\tany\t[DISABLED]\n" +
|
||||
"2\tCafe\tany\t\n" +
|
||||
"3\tFlaky\tany\t[TEMP-DISABLED]\n" +
|
||||
"bogus\tline\tany\t\n"
|
||||
|
||||
networks := parseWpaListNetworks(text)
|
||||
|
||||
assert.Len(t, networks, 4)
|
||||
assert.Equal(t, 0, networks[0].id)
|
||||
assert.Equal(t, "Home", networks[0].ssid)
|
||||
assert.True(t, networks[0].current)
|
||||
assert.False(t, networks[0].disabled)
|
||||
|
||||
assert.True(t, networks[1].disabled)
|
||||
assert.False(t, networks[2].disabled)
|
||||
assert.False(t, networks[2].current)
|
||||
assert.True(t, networks[3].tempDisabled)
|
||||
}
|
||||
|
||||
func TestParseWpaEventLine(t *testing.T) {
|
||||
testCases := []struct {
|
||||
line string
|
||||
ok bool
|
||||
priority int
|
||||
name string
|
||||
args string
|
||||
}{
|
||||
{"<2>CTRL-EVENT-CONNECTED - Connection to 02:00:01:02:03:04 completed [id=0 id_str=]", true, 2, "CTRL-EVENT-CONNECTED", "- Connection to 02:00:01:02:03:04 completed [id=0 id_str=]"},
|
||||
{"<3>CTRL-EVENT-DISCONNECTED bssid=02:00:01:02:03:04 reason=3 locally_generated=1", true, 3, "CTRL-EVENT-DISCONNECTED", "bssid=02:00:01:02:03:04 reason=3 locally_generated=1"},
|
||||
{"<2>CTRL-EVENT-SCAN-RESULTS ", true, 2, "CTRL-EVENT-SCAN-RESULTS", ""},
|
||||
{"OK", false, 0, "", ""},
|
||||
{"", false, 0, "", ""},
|
||||
{"<x>BROKEN", false, 0, "", ""},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
event, ok := parseWpaEventLine(tc.line)
|
||||
assert.Equal(t, tc.ok, ok, "line=%q", tc.line)
|
||||
if !tc.ok {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, tc.priority, event.priority, "line=%q", tc.line)
|
||||
assert.Equal(t, tc.name, event.name, "line=%q", tc.line)
|
||||
assert.Equal(t, tc.args, event.args, "line=%q", tc.line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWpaTempDisabled(t *testing.T) {
|
||||
ssid, reason := parseWpaTempDisabled(`id=1 ssid="Home Net" auth_failures=1 duration=10 reason=WRONG_KEY`)
|
||||
assert.Equal(t, "Home Net", ssid)
|
||||
assert.Equal(t, "WRONG_KEY", reason)
|
||||
|
||||
ssid, reason = parseWpaTempDisabled(`id=2 ssid="quo\"te" auth_failures=3 duration=20 reason=CONN_FAILED`)
|
||||
assert.Equal(t, `quo"te`, ssid)
|
||||
assert.Equal(t, "CONN_FAILED", reason)
|
||||
|
||||
ssid, reason = parseWpaTempDisabled("garbage")
|
||||
assert.Empty(t, ssid)
|
||||
assert.Empty(t, reason)
|
||||
}
|
||||
|
||||
func TestDecodeWpaSSIDText(t *testing.T) {
|
||||
testCases := []struct {
|
||||
encoded string
|
||||
expected string
|
||||
}{
|
||||
{"Plain", "Plain"},
|
||||
{`With\tTab`, "With\tTab"},
|
||||
{`With\\Backslash`, `With\Backslash`},
|
||||
{`With\"Quote`, `With"Quote`},
|
||||
{`Hex\xc3\xa9`, "Hexé"},
|
||||
{`Trailing\`, `Trailing\`},
|
||||
{`Bad\xzz`, `Bad\xzz`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t, tc.expected, decodeWpaSSIDText(tc.encoded), "encoded=%q", tc.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWpaQuotedString(t *testing.T) {
|
||||
quoted, err := wpaQuotedString("passphrase")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `"passphrase"`, quoted)
|
||||
|
||||
_, err = wpaQuotedString(`with"quote`)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = wpaQuotedString("with\nnewline")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParseWpaConfigPassphrase(t *testing.T) {
|
||||
config := "ctrl_interface=/var/run/wpa_supplicant\n" +
|
||||
"update_config=1\n" +
|
||||
"\n" +
|
||||
"network={\n" +
|
||||
"\tssid=\"Home\"\n" +
|
||||
"\tpsk=\"secret pass\"\n" +
|
||||
"}\n" +
|
||||
"\n" +
|
||||
"network={\n" +
|
||||
"\tssid=486578\n" +
|
||||
"\tpsk=\"hexpass\"\n" +
|
||||
"}\n" +
|
||||
"\n" +
|
||||
"network={\n" +
|
||||
"\tssid=\"RawKey\"\n" +
|
||||
"\tpsk=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n" +
|
||||
"}\n"
|
||||
|
||||
pass, err := parseWpaConfigPassphrase(config, "Home")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "secret pass", pass)
|
||||
|
||||
pass, err = parseWpaConfigPassphrase(config, "Hex")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hexpass", pass)
|
||||
|
||||
_, err = parseWpaConfigPassphrase(config, "RawKey")
|
||||
assert.Error(t, err, "raw hex PSK is not a passphrase")
|
||||
|
||||
_, err = parseWpaConfigPassphrase(config, "Unknown")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func startFakeWpaSocket(t *testing.T, handler func(cmd string) []string) string {
|
||||
t.Helper()
|
||||
|
||||
sockPath := filepath.Join(t.TempDir(), "wlan0")
|
||||
conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: sockPath, Net: "unixgram"})
|
||||
if err != nil {
|
||||
t.Fatalf("listen fake wpa socket: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, addr, err := conn.ReadFromUnix(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, reply := range handler(string(buf[:n])) {
|
||||
conn.WriteToUnix([]byte(reply), addr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return sockPath
|
||||
}
|
||||
|
||||
func TestWpaCtrlConn_Request(t *testing.T) {
|
||||
sockPath := startFakeWpaSocket(t, func(cmd string) []string {
|
||||
switch cmd {
|
||||
case "PING":
|
||||
return []string{"PONG\n"}
|
||||
case "SCAN":
|
||||
return []string{"<2>CTRL-EVENT-SCAN-STARTED ", "OK\n"}
|
||||
default:
|
||||
return []string{"FAIL\n"}
|
||||
}
|
||||
})
|
||||
|
||||
conn, err := newWpaCtrlConn(sockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open wpa_ctrl conn: %v", err)
|
||||
}
|
||||
defer conn.close()
|
||||
|
||||
reply, err := conn.request("PING")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "PONG", reply)
|
||||
|
||||
reply, err = conn.request("SCAN")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "OK", reply, "unsolicited event datagrams must be skipped")
|
||||
|
||||
reply, err = conn.request("BOGUS")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "FAIL", reply)
|
||||
}
|
||||
|
||||
func TestWpaSupplicantBackend_ClassifyAttempt(t *testing.T) {
|
||||
backend, _ := NewWpaSupplicantBackend()
|
||||
|
||||
att := &wpaConnectAttempt{ssid: "Home", sawTempDisabled: true}
|
||||
assert.Equal(t, "bad-credentials", backend.classifyAttempt(att))
|
||||
|
||||
att = &wpaConnectAttempt{ssid: "Home"}
|
||||
assert.Equal(t, "no-such-ssid", backend.classifyAttempt(att))
|
||||
|
||||
backend.recentScansMu.Lock()
|
||||
backend.recentScans["Home"] = time.Now()
|
||||
backend.recentScansMu.Unlock()
|
||||
assert.Equal(t, "assoc-timeout", backend.classifyAttempt(att))
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package network
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (b *WpaSupplicantBackend) ConnectEthernet() error {
|
||||
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectEthernet() error {
|
||||
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectEthernetDevice(device string) error {
|
||||
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ActivateWiredConnection(uuid string) error {
|
||||
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ListActiveVPN() ([]VPNActive, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectVPN(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectAllVPN() error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ClearVPNCredentials(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ListVPNPlugins() ([]VPNPlugin, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ImportVPN(filePath string, name string) (*VPNImportResult, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetVPNConfig(uuidOrName string) (*VPNConfig, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) UpdateVPNConfig(uuid string, updates map[string]any) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DeleteVPN(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) SetVPNCredentials(uuid, username, password string, save bool) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ScanWiFiDevice(device string) error {
|
||||
return b.ScanWiFi()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectWiFiDevice(device string) error {
|
||||
return b.DisconnectWiFi()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetWiFiDevices() []WiFiDevice {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
return b.getWiFiDevicesLocked()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) getWiFiDevicesLocked() []WiFiDevice {
|
||||
if b.state.WiFiDevice == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
stateStr := "disconnected"
|
||||
if b.state.WiFiConnected {
|
||||
stateStr = "connected"
|
||||
}
|
||||
|
||||
return []WiFiDevice{{
|
||||
Name: b.state.WiFiDevice,
|
||||
State: stateStr,
|
||||
Connected: b.state.WiFiConnected,
|
||||
SSID: b.state.WiFiSSID,
|
||||
BSSID: b.state.WiFiBSSID,
|
||||
Signal: b.state.WiFiSignal,
|
||||
IP: b.state.WiFiIP,
|
||||
Networks: b.state.WiFiNetworks,
|
||||
}}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
@@ -14,6 +15,7 @@ const (
|
||||
BackendIwd
|
||||
BackendConnMan
|
||||
BackendNetworkd
|
||||
BackendWpaSupplicant
|
||||
)
|
||||
|
||||
func nameHasOwner(bus *dbus.Conn, name string) (bool, error) {
|
||||
@@ -35,10 +37,24 @@ type DetectResult struct {
|
||||
ChosenReason string
|
||||
}
|
||||
|
||||
func wpaSupplicantCtrlDirPresent() bool {
|
||||
info, err := os.Stat(wpaSupplicantCtrlDir)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
func DetectNetworkStack() (*DetectResult, error) {
|
||||
bus, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect system bus: %w", err)
|
||||
// FreeBSD and minimal Linux systems may run wpa_supplicant with no
|
||||
// system bus at all; the control directory is the only signal there.
|
||||
if !wpaSupplicantCtrlDirPresent() {
|
||||
return nil, fmt.Errorf("connect system bus: %w", err)
|
||||
}
|
||||
return &DetectResult{
|
||||
Backend: BackendWpaSupplicant,
|
||||
HasWpaSupp: true,
|
||||
ChosenReason: "System bus unreachable; wpa_supplicant control directory present. Using wpa_ctrl interface.",
|
||||
}, nil
|
||||
}
|
||||
defer bus.Close()
|
||||
|
||||
@@ -76,10 +92,14 @@ func DetectNetworkStack() (*DetectResult, error) {
|
||||
case hasNetworkd:
|
||||
res.Backend = BackendNetworkd
|
||||
res.ChosenReason = "systemd-networkd detected (no NM/ConnMan). Using networkd for L3 and wired."
|
||||
case wpaSupplicantCtrlDirPresent():
|
||||
res.Backend = BackendWpaSupplicant
|
||||
res.HasWpaSupp = true
|
||||
res.ChosenReason = "No NM/ConnMan/iwd/networkd; wpa_supplicant control directory present. Using wpa_ctrl interface."
|
||||
default:
|
||||
res.Backend = BackendNone
|
||||
if hasWpa {
|
||||
res.ChosenReason = "No NM/ConnMan/iwd; wpa_supplicant present. Consider a wpa_supplicant path."
|
||||
res.ChosenReason = "No NM/ConnMan/iwd; wpa_supplicant D-Bus name present but control directory missing."
|
||||
} else {
|
||||
res.ChosenReason = "No known network manager bus names found."
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ func TestBackendType_Constants(t *testing.T) {
|
||||
assert.Equal(t, BackendType(2), BackendIwd)
|
||||
assert.Equal(t, BackendType(3), BackendConnMan)
|
||||
assert.Equal(t, BackendType(4), BackendNetworkd)
|
||||
assert.Equal(t, BackendType(5), BackendWpaSupplicant)
|
||||
}
|
||||
|
||||
func TestDetectResult_HasNetworkdField(t *testing.T) {
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
)
|
||||
|
||||
// ErrNoNetworkBackend is returned when no supported network management daemon
|
||||
// (NetworkManager, iwd, systemd-networkd, ConnMan) is present on the system.
|
||||
// (NetworkManager, iwd, systemd-networkd, ConnMan, wpa_supplicant) is present
|
||||
// on the system.
|
||||
var ErrNoNetworkBackend = errors.New("no supported network backend found")
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
@@ -62,6 +63,13 @@ func NewManager() (*Manager, error) {
|
||||
backend = nd
|
||||
}
|
||||
|
||||
case BackendWpaSupplicant:
|
||||
wpa, err := NewWpaSupplicantBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create wpa_supplicant backend: %w", err)
|
||||
}
|
||||
backend = wpa
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoNetworkBackend, detection.ChosenReason)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const resumeDelay = 3 * time.Second
|
||||
@@ -95,21 +94,3 @@ func (m *Manager) Close() {
|
||||
}
|
||||
log.Info("TrayRecovery manager closed")
|
||||
}
|
||||
|
||||
// timeSuspended returns how long the system has spent in suspend since boot.
|
||||
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
|
||||
// CLOCK_MONOTONIC (excludes suspend).
|
||||
func timeSuspended() time.Duration {
|
||||
var bt, mt unix.Timespec
|
||||
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
|
||||
return 0
|
||||
}
|
||||
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
|
||||
return 0
|
||||
}
|
||||
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
|
||||
if diff < 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(diff)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package trayrecovery
|
||||
|
||||
import "time"
|
||||
|
||||
// FreeBSD has no CLOCK_BOOTTIME (clock_gettime(2)), so suspend time since
|
||||
// boot cannot be measured; the startup recovery scan is skipped.
|
||||
func timeSuspended() time.Duration {
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package trayrecovery
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// timeSuspended returns how long the system has spent in suspend since boot.
|
||||
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
|
||||
// CLOCK_MONOTONIC (excludes suspend).
|
||||
func timeSuspended() time.Duration {
|
||||
var bt, mt unix.Timespec
|
||||
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
|
||||
return 0
|
||||
}
|
||||
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
|
||||
return 0
|
||||
}
|
||||
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
|
||||
if diff < 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(diff)
|
||||
}
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||
)
|
||||
|
||||
const animKelvinStep = 25
|
||||
@@ -920,7 +920,7 @@ func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
|
||||
return fmt.Errorf("gamma control invalid")
|
||||
}
|
||||
|
||||
fd, err := MemfdCreate("gamma-ramp", 0)
|
||||
fd, err := shm.CreateAnonFd("gamma-ramp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1295,11 +1295,3 @@ func (m *Manager) Close() {
|
||||
m.dbusConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func MemfdCreate(name string, flags int) (int, error) {
|
||||
fd, err := unix.MemfdCreate(name, flags)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user