mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-13 00:42:49 -05:00
rename backend to core
This commit is contained in:
552
core/internal/server/network/API.md
Normal file
552
core/internal/server/network/API.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# NetworkManager API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The network manager API provides methods for managing WiFi connections, monitoring network state, and handling credential prompts through NetworkManager. Communication occurs over a message-based protocol (websocket, IPC, etc.) with event subscriptions for state updates.
|
||||
|
||||
## API Methods
|
||||
|
||||
### network.wifi.connect
|
||||
|
||||
Initiate a WiFi connection.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"method": "network.wifi.connect",
|
||||
"params": {
|
||||
"ssid": "NetworkName",
|
||||
"password": "optional-password",
|
||||
"interactive": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `ssid` (string, required): Network SSID
|
||||
- `password` (string, optional): Pre-shared key for WPA/WPA2/WPA3 networks
|
||||
- `interactive` (boolean, optional): Enable credential prompting if authentication fails or password is missing. Automatically set to `true` when connecting to secured networks without providing a password.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "connecting"
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Returns immediately; connection happens asynchronously
|
||||
- State updates delivered via `network` service subscription
|
||||
- Credential prompts delivered via `network.credentials` service subscription
|
||||
|
||||
### network.credentials.submit
|
||||
|
||||
Submit credentials in response to a prompt.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"method": "network.credentials.submit",
|
||||
"params": {
|
||||
"token": "correlation-token",
|
||||
"secrets": {
|
||||
"psk": "password"
|
||||
},
|
||||
"save": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `token` (string, required): Token from credential prompt
|
||||
- `secrets` (object, required): Key-value map of credential fields
|
||||
- `save` (boolean, optional): Whether to persist credentials (default: false)
|
||||
|
||||
**Common secret fields:**
|
||||
- `psk`: Pre-shared key for WPA2/WPA3 personal networks
|
||||
- `identity`: Username for 802.1X enterprise networks
|
||||
- `password`: Password for 802.1X enterprise networks
|
||||
|
||||
### network.credentials.cancel
|
||||
|
||||
Cancel a credential prompt.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"method": "network.credentials.cancel",
|
||||
"params": {
|
||||
"token": "correlation-token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Subscriptions
|
||||
|
||||
### Subscribing to Events
|
||||
|
||||
Subscribe to receive network state updates and credential prompts:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "subscribe",
|
||||
"params": {
|
||||
"services": ["network", "network.credentials"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both services are required for full connection handling. Missing `network.credentials` means credential prompts won't be received.
|
||||
|
||||
### network Service Events
|
||||
|
||||
State updates are sent whenever network configuration changes:
|
||||
|
||||
```json
|
||||
{
|
||||
"service": "network",
|
||||
"data": {
|
||||
"networkStatus": "wifi",
|
||||
"isConnecting": false,
|
||||
"connectingSSID": "",
|
||||
"wifiConnected": true,
|
||||
"wifiSSID": "MyNetwork",
|
||||
"wifiIP": "192.168.1.100",
|
||||
"lastError": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**State fields:**
|
||||
- `networkStatus`: Current connection type (`wifi`, `ethernet`, `disconnected`)
|
||||
- `isConnecting`: Whether a connection attempt is in progress
|
||||
- `connectingSSID`: SSID being connected to (empty when idle)
|
||||
- `wifiConnected`: Whether associated with an access point
|
||||
- `wifiSSID`: Currently connected network name
|
||||
- `wifiIP`: Assigned IP address (empty until DHCP completes)
|
||||
- `lastError`: Error message from last failed connection attempt
|
||||
|
||||
### network.credentials Service Events
|
||||
|
||||
Credential prompts are sent when authentication is required:
|
||||
|
||||
```json
|
||||
{
|
||||
"service": "network.credentials",
|
||||
"data": {
|
||||
"token": "unique-prompt-id",
|
||||
"ssid": "NetworkName",
|
||||
"setting": "802-11-wireless-security",
|
||||
"fields": ["psk"],
|
||||
"hints": ["wpa3", "sae"],
|
||||
"reason": "Credentials required"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Prompt fields:**
|
||||
- `token`: Unique identifier for this prompt (use in submit/cancel)
|
||||
- `ssid`: Network requesting credentials
|
||||
- `setting`: Authentication type (`802-11-wireless-security` for personal WiFi, `802-1x` for enterprise)
|
||||
- `fields`: Array of required credential field names
|
||||
- `hints`: Additional context about the network type
|
||||
- `reason`: Human-readable explanation (e.g., "Previous password was incorrect")
|
||||
|
||||
## Connection Flow
|
||||
|
||||
### Typical Timeline
|
||||
|
||||
```
|
||||
T+0ms Call network.wifi.connect
|
||||
T+10ms Receive {"success": true, "message": "connecting"}
|
||||
T+100ms State update: isConnecting=true, connectingSSID="Network"
|
||||
T+500ms Credential prompt (if needed)
|
||||
T+1000ms Submit credentials
|
||||
T+3000ms State update: wifiConnected=true, wifiIP="192.168.x.x"
|
||||
```
|
||||
|
||||
### State Machine
|
||||
|
||||
```
|
||||
IDLE
|
||||
|
|
||||
| network.wifi.connect
|
||||
v
|
||||
CONNECTING (isConnecting=true, connectingSSID set)
|
||||
|
|
||||
+-- Needs credentials
|
||||
| |
|
||||
| v
|
||||
| PROMPTING (credential prompt event)
|
||||
| |
|
||||
| | network.credentials.submit
|
||||
| v
|
||||
| back to CONNECTING
|
||||
|
|
||||
+-- Success
|
||||
| |
|
||||
| v
|
||||
| CONNECTED (wifiConnected=true, wifiIP set, isConnecting=false)
|
||||
|
|
||||
+-- Failure
|
||||
|
|
||||
v
|
||||
ERROR (isConnecting=false, !wifiConnected, lastError set)
|
||||
```
|
||||
|
||||
## Connection Success Detection
|
||||
|
||||
A connection is successful when all of the following are true:
|
||||
|
||||
1. `wifiConnected` is `true`
|
||||
2. `wifiIP` is set and non-empty
|
||||
3. `wifiSSID` matches the target network
|
||||
4. `isConnecting` is `false`
|
||||
|
||||
Do not rely on `wifiConnected` alone - the device may be associated with an access point but not have an IP address yet.
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
function isConnectionComplete(state, targetSSID) {
|
||||
return state.wifiConnected &&
|
||||
state.wifiIP &&
|
||||
state.wifiIP !== "" &&
|
||||
state.wifiSSID === targetSSID &&
|
||||
!state.isConnecting;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Detection
|
||||
|
||||
Errors occur when a connection attempt stops without success:
|
||||
|
||||
```javascript
|
||||
function checkForFailure(state, wasConnecting, targetSSID) {
|
||||
// Was connecting, now idle, but not connected
|
||||
if (wasConnecting &&
|
||||
!state.isConnecting &&
|
||||
state.connectingSSID === "" &&
|
||||
!state.wifiConnected) {
|
||||
return state.lastError || "Connection failed";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Common Error Scenarios
|
||||
|
||||
#### Wrong Password
|
||||
|
||||
**Detection methods:**
|
||||
|
||||
1. Quick failure (< 3 seconds from start)
|
||||
2. `lastError` contains "password", "auth", or "secrets"
|
||||
3. Second credential prompt with `reason: "Previous password was incorrect"`
|
||||
|
||||
**Handling:**
|
||||
```javascript
|
||||
if (prompt.reason === "Previous password was incorrect") {
|
||||
// Show error, clear password field, re-focus input
|
||||
}
|
||||
```
|
||||
|
||||
#### Network Out of Range
|
||||
|
||||
**Detection:**
|
||||
- `lastError` contains "not-found" or "connection-attempt-failed"
|
||||
|
||||
#### Connection Timeout
|
||||
|
||||
**Detection:**
|
||||
- `isConnecting` remains true for > 30 seconds
|
||||
|
||||
**Implementation:**
|
||||
```javascript
|
||||
let timeout = setTimeout(() => {
|
||||
if (currentState.isConnecting) {
|
||||
handleTimeout();
|
||||
}
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
#### DHCP Failure
|
||||
|
||||
**Detection:**
|
||||
- `wifiConnected` is true
|
||||
- `wifiIP` is empty after 15+ seconds
|
||||
|
||||
### Error Message Translation
|
||||
|
||||
Map technical errors to user-friendly messages:
|
||||
|
||||
| lastError value | Meaning | User message |
|
||||
|----------------|---------|--------------|
|
||||
| `secrets-required` | Password needed | "Please enter password" |
|
||||
| `authentication-failed` | Wrong password | "Incorrect password" |
|
||||
| `connection-removed` | Profile deleted | "Network configuration removed" |
|
||||
| `connection-attempt-failed` | Generic failure | "Failed to connect" |
|
||||
| `network-not-found` | Out of range | "Network not found" |
|
||||
| `(timeout)` | Timeout | "Connection timed out" |
|
||||
|
||||
## Credential Handling
|
||||
|
||||
### Secret Agent Architecture
|
||||
|
||||
The credential system uses a broker pattern:
|
||||
|
||||
```
|
||||
NetworkManager -> SecretAgent -> PromptBroker -> UI -> User
|
||||
^
|
||||
|
|
||||
User Response
|
||||
|
|
||||
NetworkManager <- SecretAgent <- PromptBroker <- UI
|
||||
```
|
||||
|
||||
### Implementing a Broker
|
||||
|
||||
```go
|
||||
type CustomBroker struct {
|
||||
ui UIInterface
|
||||
pending map[string]chan network.PromptReply
|
||||
}
|
||||
|
||||
func (b *CustomBroker) Ask(ctx context.Context, req network.PromptRequest) (string, error) {
|
||||
token := generateToken()
|
||||
b.pending[token] = make(chan network.PromptReply, 1)
|
||||
|
||||
// Send to UI
|
||||
b.ui.ShowCredentialPrompt(token, req)
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (b *CustomBroker) Wait(ctx context.Context, token string) (network.PromptReply, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return network.PromptReply{}, errors.New("timeout")
|
||||
case reply := <-b.pending[token]:
|
||||
return reply, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *CustomBroker) Resolve(token string, reply network.PromptReply) error {
|
||||
if ch, ok := b.pending[token]; ok {
|
||||
ch <- reply
|
||||
close(ch)
|
||||
delete(b.pending, token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Credential Field Types
|
||||
|
||||
**Personal WiFi (802-11-wireless-security):**
|
||||
- Fields: `["psk"]`
|
||||
- UI: Single password input
|
||||
|
||||
**Enterprise WiFi (802-1x):**
|
||||
- Fields: `["identity", "password"]`
|
||||
- UI: Username and password inputs
|
||||
|
||||
### Building Secrets Object
|
||||
|
||||
```javascript
|
||||
function buildSecrets(setting, fields, formData) {
|
||||
let secrets = {};
|
||||
|
||||
if (setting === "802-11-wireless-security") {
|
||||
secrets.psk = formData.password;
|
||||
} else if (setting === "802-1x") {
|
||||
secrets.identity = formData.username;
|
||||
secrets.password = formData.password;
|
||||
}
|
||||
|
||||
return secrets;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Track Target Network
|
||||
|
||||
Always store which network you're connecting to:
|
||||
|
||||
```javascript
|
||||
let targetSSID = null;
|
||||
|
||||
function connect(ssid) {
|
||||
targetSSID = ssid;
|
||||
// send request
|
||||
}
|
||||
|
||||
function onStateUpdate(state) {
|
||||
if (!targetSSID) return;
|
||||
|
||||
if (state.wifiSSID === targetSSID && state.wifiConnected && state.wifiIP) {
|
||||
// Success for the network we care about
|
||||
targetSSID = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Implement Timeouts
|
||||
|
||||
Never wait indefinitely for a connection:
|
||||
|
||||
```javascript
|
||||
const CONNECTION_TIMEOUT = 30000; // 30 seconds
|
||||
const DHCP_TIMEOUT = 15000; // 15 seconds
|
||||
|
||||
let timer = setTimeout(() => {
|
||||
if (stillConnecting) {
|
||||
handleTimeout();
|
||||
}
|
||||
}, CONNECTION_TIMEOUT);
|
||||
```
|
||||
|
||||
### Handle Credential Re-prompts
|
||||
|
||||
Wrong passwords trigger a second prompt:
|
||||
|
||||
```javascript
|
||||
function onCredentialPrompt(prompt) {
|
||||
if (prompt.reason.includes("incorrect")) {
|
||||
// Show error, but keep dialog open
|
||||
showError("Wrong password");
|
||||
clearPasswordField();
|
||||
} else {
|
||||
// First time prompt
|
||||
showDialog(prompt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Clean Up State
|
||||
|
||||
Reset tracking variables on success, failure, or cancellation:
|
||||
|
||||
```javascript
|
||||
function cleanup() {
|
||||
clearTimeout(timer);
|
||||
targetSSID = null;
|
||||
closeDialogs();
|
||||
}
|
||||
```
|
||||
|
||||
### Subscribe to Both Services
|
||||
|
||||
Missing `network.credentials` means prompts won't arrive:
|
||||
|
||||
```javascript
|
||||
// Correct
|
||||
services: ["network", "network.credentials"]
|
||||
|
||||
// Wrong - will miss credential prompts
|
||||
services: ["network"]
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Connection Test Checklist
|
||||
|
||||
- [ ] Connect to open network
|
||||
- [ ] Connect to WPA2 network with password provided
|
||||
- [ ] Connect to WPA2 network without password (triggers prompt)
|
||||
- [ ] Enter wrong password (verify error and re-prompt)
|
||||
- [ ] Cancel credential prompt
|
||||
- [ ] Connection timeout after 30 seconds
|
||||
- [ ] DHCP timeout detection
|
||||
- [ ] Network out of range
|
||||
- [ ] Reconnect to already-configured network
|
||||
|
||||
### Verifying Secret Agent Setup
|
||||
|
||||
Check connection profile flags:
|
||||
```bash
|
||||
nmcli connection show "NetworkName" | grep flags
|
||||
# Should show: 802-11-wireless-security.psk-flags: 1 (agent-owned)
|
||||
```
|
||||
|
||||
Check agent registration in logs:
|
||||
```
|
||||
INFO: Registered with NetworkManager as secret agent
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Never log credential values (passwords, PSKs)
|
||||
- Clear password fields when dialogs close
|
||||
- Implement prompt timeouts (default: 2 minutes)
|
||||
- Validate user input before submission
|
||||
- Use secure channels for credential transmission
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Credential prompt doesn't appear
|
||||
|
||||
**Check:**
|
||||
- Subscribed to both `network` and `network.credentials`
|
||||
- Connection has `interactive: true`
|
||||
- Secret flags set to AGENT_OWNED (value: 1)
|
||||
- Broker registered successfully
|
||||
|
||||
### Connection succeeds without prompting
|
||||
|
||||
**Cause:** NetworkManager found saved credentials
|
||||
|
||||
**Solution:** Delete existing connection first, or use different credentials
|
||||
|
||||
### State updates seem delayed
|
||||
|
||||
**Expected behavior:** State changes occur in rapid succession during connection
|
||||
|
||||
**Solution:** Debounce UI updates; only act on final state
|
||||
|
||||
### Multiple rapid credential prompts
|
||||
|
||||
**Cause:** Connection profile has incorrect flags or conflicting agents
|
||||
|
||||
**Solution:**
|
||||
- Check only one agent is running
|
||||
- Verify psk-flags value
|
||||
- Check NetworkManager logs for agent conflicts
|
||||
|
||||
## Data Structures Reference
|
||||
|
||||
### PromptRequest
|
||||
```go
|
||||
type PromptRequest struct {
|
||||
SSID string `json:"ssid"`
|
||||
SettingName string `json:"setting"`
|
||||
Fields []string `json:"fields"`
|
||||
Hints []string `json:"hints"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
```
|
||||
|
||||
### PromptReply
|
||||
```go
|
||||
type PromptReply struct {
|
||||
Secrets map[string]string `json:"secrets"`
|
||||
Save bool `json:"save"`
|
||||
Cancel bool `json:"cancel"`
|
||||
}
|
||||
```
|
||||
|
||||
### NetworkState
|
||||
```go
|
||||
type NetworkState struct {
|
||||
NetworkStatus string `json:"networkStatus"`
|
||||
IsConnecting bool `json:"isConnecting"`
|
||||
ConnectingSSID string `json:"connectingSSID"`
|
||||
WifiConnected bool `json:"wifiConnected"`
|
||||
WifiSSID string `json:"wifiSSID"`
|
||||
WifiIP string `json:"wifiIP"`
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
```
|
||||
306
core/internal/server/network/agent_iwd.go
Normal file
306
core/internal/server/network/agent_iwd.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
iwdAgentManagerPath = "/net/connman/iwd"
|
||||
iwdAgentManagerIface = "net.connman.iwd.AgentManager"
|
||||
iwdAgentInterface = "net.connman.iwd.Agent"
|
||||
iwdAgentObjectPath = "/com/danklinux/iwdagent"
|
||||
)
|
||||
|
||||
type ConnectionStateChecker interface {
|
||||
IsConnectingTo(ssid string) bool
|
||||
}
|
||||
|
||||
type IWDAgent struct {
|
||||
conn *dbus.Conn
|
||||
objPath dbus.ObjectPath
|
||||
prompts PromptBroker
|
||||
onUserCanceled func()
|
||||
onPromptRetry func(ssid string)
|
||||
lastRequestSSID string
|
||||
stateChecker ConnectionStateChecker
|
||||
}
|
||||
|
||||
const iwdAgentIntrospectXML = `
|
||||
<node>
|
||||
<interface name="net.connman.iwd.Agent">
|
||||
<method name="Release">
|
||||
<annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
|
||||
</method>
|
||||
<method name="RequestPassphrase">
|
||||
<arg type="o" name="network" direction="in"/>
|
||||
<arg type="s" name="passphrase" direction="out"/>
|
||||
</method>
|
||||
<method name="RequestPrivateKeyPassphrase">
|
||||
<arg type="o" name="network" direction="in"/>
|
||||
<arg type="s" name="passphrase" direction="out"/>
|
||||
</method>
|
||||
<method name="RequestUserNameAndPassword">
|
||||
<arg type="o" name="network" direction="in"/>
|
||||
<arg type="s" name="username" direction="out"/>
|
||||
<arg type="s" name="password" direction="out"/>
|
||||
</method>
|
||||
<method name="RequestUserPassword">
|
||||
<arg type="o" name="network" direction="in"/>
|
||||
<arg type="s" name="user" direction="in"/>
|
||||
<arg type="s" name="password" direction="out"/>
|
||||
</method>
|
||||
<method name="Cancel">
|
||||
<arg type="s" name="reason" direction="in"/>
|
||||
<annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>`
|
||||
|
||||
func NewIWDAgent(conn *dbus.Conn, prompts PromptBroker) (*IWDAgent, error) {
|
||||
if conn == nil {
|
||||
return nil, fmt.Errorf("dbus connection is nil")
|
||||
}
|
||||
|
||||
agent := &IWDAgent{
|
||||
conn: conn,
|
||||
objPath: dbus.ObjectPath(iwdAgentObjectPath),
|
||||
prompts: prompts,
|
||||
}
|
||||
|
||||
if err := conn.Export(agent, agent.objPath, iwdAgentInterface); err != nil {
|
||||
return nil, fmt.Errorf("failed to export IWD agent: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.Export(agent, agent.objPath, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
return nil, fmt.Errorf("failed to export introspection: %w", err)
|
||||
}
|
||||
|
||||
mgr := conn.Object("net.connman.iwd", dbus.ObjectPath(iwdAgentManagerPath))
|
||||
call := mgr.Call(iwdAgentManagerIface+".RegisterAgent", 0, agent.objPath)
|
||||
if call.Err != nil {
|
||||
return nil, fmt.Errorf("failed to register agent with iwd: %w", call.Err)
|
||||
}
|
||||
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func (a *IWDAgent) Close() {
|
||||
if a.conn != nil {
|
||||
mgr := a.conn.Object("net.connman.iwd", dbus.ObjectPath(iwdAgentManagerPath))
|
||||
mgr.Call(iwdAgentManagerIface+".UnregisterAgent", 0, a.objPath)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *IWDAgent) SetStateChecker(checker ConnectionStateChecker) {
|
||||
a.stateChecker = checker
|
||||
}
|
||||
|
||||
func (a *IWDAgent) getNetworkName(networkPath dbus.ObjectPath) string {
|
||||
netObj := a.conn.Object("net.connman.iwd", networkPath)
|
||||
nameVar, err := netObj.GetProperty("net.connman.iwd.Network.Name")
|
||||
if err == nil {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return string(networkPath)
|
||||
}
|
||||
|
||||
func (a *IWDAgent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Error) {
|
||||
ssid := a.getNetworkName(network)
|
||||
|
||||
if a.stateChecker != nil && !a.stateChecker.IsConnectingTo(ssid) {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if a.prompts == nil {
|
||||
if a.onUserCanceled != nil {
|
||||
a.onUserCanceled()
|
||||
}
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if a.lastRequestSSID == ssid {
|
||||
if a.onPromptRetry != nil {
|
||||
a.onPromptRetry(ssid)
|
||||
}
|
||||
}
|
||||
a.lastRequestSSID = ssid
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
token, err := a.prompts.Ask(ctx, PromptRequest{
|
||||
SSID: ssid,
|
||||
Fields: []string{"psk"},
|
||||
})
|
||||
if err != nil {
|
||||
if a.onUserCanceled != nil {
|
||||
a.onUserCanceled()
|
||||
}
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
reply, err := a.prompts.Wait(ctx, token)
|
||||
if err != nil {
|
||||
if reply.Cancel || errors.Is(err, errdefs.ErrSecretPromptCancelled) {
|
||||
if a.onUserCanceled != nil {
|
||||
a.onUserCanceled()
|
||||
}
|
||||
}
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if passphrase, ok := reply.Secrets["psk"]; ok {
|
||||
return passphrase, nil
|
||||
}
|
||||
|
||||
if a.onUserCanceled != nil {
|
||||
a.onUserCanceled()
|
||||
}
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
func (a *IWDAgent) RequestPrivateKeyPassphrase(network dbus.ObjectPath) (string, *dbus.Error) {
|
||||
ssid := a.getNetworkName(network)
|
||||
|
||||
if a.stateChecker != nil && !a.stateChecker.IsConnectingTo(ssid) {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if a.prompts == nil {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if a.lastRequestSSID == ssid {
|
||||
if a.onPromptRetry != nil {
|
||||
a.onPromptRetry(ssid)
|
||||
}
|
||||
}
|
||||
a.lastRequestSSID = ssid
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
token, err := a.prompts.Ask(ctx, PromptRequest{
|
||||
SSID: ssid,
|
||||
Fields: []string{"private-key-password"},
|
||||
})
|
||||
if err != nil {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
reply, err := a.prompts.Wait(ctx, token)
|
||||
if err != nil || reply.Cancel {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if passphrase, ok := reply.Secrets["private-key-password"]; ok {
|
||||
return passphrase, nil
|
||||
}
|
||||
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
func (a *IWDAgent) RequestUserNameAndPassword(network dbus.ObjectPath) (string, string, *dbus.Error) {
|
||||
ssid := a.getNetworkName(network)
|
||||
|
||||
if a.stateChecker != nil && !a.stateChecker.IsConnectingTo(ssid) {
|
||||
return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if a.prompts == nil {
|
||||
return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if a.lastRequestSSID == ssid {
|
||||
if a.onPromptRetry != nil {
|
||||
a.onPromptRetry(ssid)
|
||||
}
|
||||
}
|
||||
a.lastRequestSSID = ssid
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
token, err := a.prompts.Ask(ctx, PromptRequest{
|
||||
SSID: ssid,
|
||||
Fields: []string{"identity", "password"},
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
reply, err := a.prompts.Wait(ctx, token)
|
||||
if err != nil || reply.Cancel {
|
||||
return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
username, hasUser := reply.Secrets["identity"]
|
||||
password, hasPass := reply.Secrets["password"]
|
||||
|
||||
if hasUser && hasPass {
|
||||
return username, password, nil
|
||||
}
|
||||
|
||||
return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
func (a *IWDAgent) RequestUserPassword(network dbus.ObjectPath, user string) (string, *dbus.Error) {
|
||||
ssid := a.getNetworkName(network)
|
||||
|
||||
if a.stateChecker != nil && !a.stateChecker.IsConnectingTo(ssid) {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if a.prompts == nil {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if a.lastRequestSSID == ssid {
|
||||
if a.onPromptRetry != nil {
|
||||
a.onPromptRetry(ssid)
|
||||
}
|
||||
}
|
||||
a.lastRequestSSID = ssid
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
token, err := a.prompts.Ask(ctx, PromptRequest{
|
||||
SSID: ssid,
|
||||
Fields: []string{"password"},
|
||||
})
|
||||
if err != nil {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
reply, err := a.prompts.Wait(ctx, token)
|
||||
if err != nil || reply.Cancel {
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
if password, ok := reply.Secrets["password"]; ok {
|
||||
return password, nil
|
||||
}
|
||||
|
||||
return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil)
|
||||
}
|
||||
|
||||
func (a *IWDAgent) Cancel(reason string) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *IWDAgent) Release() *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *IWDAgent) Introspect() (string, *dbus.Error) {
|
||||
return iwdAgentIntrospectXML, nil
|
||||
}
|
||||
528
core/internal/server/network/agent_networkmanager.go
Normal file
528
core/internal/server/network/agent_networkmanager.go
Normal file
@@ -0,0 +1,528 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
nmAgentManagerPath = "/org/freedesktop/NetworkManager/AgentManager"
|
||||
nmAgentManagerIface = "org.freedesktop.NetworkManager.AgentManager"
|
||||
nmSecretAgentIface = "org.freedesktop.NetworkManager.SecretAgent"
|
||||
agentObjectPath = "/org/freedesktop/NetworkManager/SecretAgent"
|
||||
agentIdentifier = "com.danklinux.NMAgent"
|
||||
)
|
||||
|
||||
type SecretAgent struct {
|
||||
conn *dbus.Conn
|
||||
objPath dbus.ObjectPath
|
||||
id string
|
||||
prompts PromptBroker
|
||||
manager *Manager
|
||||
backend *NetworkManagerBackend
|
||||
}
|
||||
|
||||
type nmVariantMap map[string]dbus.Variant
|
||||
type nmSettingMap map[string]nmVariantMap
|
||||
|
||||
const introspectXML = `
|
||||
<node>
|
||||
<interface name="org.freedesktop.NetworkManager.SecretAgent">
|
||||
<method name="GetSecrets">
|
||||
<arg type="a{sa{sv}}" name="connection" direction="in"/>
|
||||
<arg type="o" name="connection_path" direction="in"/>
|
||||
<arg type="s" name="setting_name" direction="in"/>
|
||||
<arg type="as" name="hints" direction="in"/>
|
||||
<arg type="u" name="flags" direction="in"/>
|
||||
<arg type="a{sa{sv}}" name="secrets" direction="out"/>
|
||||
</method>
|
||||
<method name="DeleteSecrets">
|
||||
<arg type="a{sa{sv}}" name="connection" direction="in"/>
|
||||
<arg type="o" name="connection_path" direction="in"/>
|
||||
</method>
|
||||
<method name="DeleteSecrets2">
|
||||
<arg type="o" name="connection_path" direction="in"/>
|
||||
<arg type="s" name="setting" direction="in"/>
|
||||
</method>
|
||||
<method name="CancelGetSecrets">
|
||||
<arg type="o" name="connection_path" direction="in"/>
|
||||
<arg type="s" name="setting_name" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.DBus.Introspectable">
|
||||
<method name="Introspect">
|
||||
<arg name="data" type="s" direction="out"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>`
|
||||
|
||||
func NewSecretAgent(prompts PromptBroker, manager *Manager, backend *NetworkManagerBackend) (*SecretAgent, error) {
|
||||
c, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||
}
|
||||
|
||||
sa := &SecretAgent{
|
||||
conn: c,
|
||||
objPath: dbus.ObjectPath(agentObjectPath),
|
||||
id: agentIdentifier,
|
||||
prompts: prompts,
|
||||
manager: manager,
|
||||
backend: backend,
|
||||
}
|
||||
|
||||
if err := c.Export(sa, sa.objPath, nmSecretAgentIface); err != nil {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("failed to export secret agent: %w", err)
|
||||
}
|
||||
|
||||
if err := c.Export(sa, sa.objPath, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("failed to export introspection: %w", err)
|
||||
}
|
||||
|
||||
mgr := c.Object("org.freedesktop.NetworkManager", dbus.ObjectPath(nmAgentManagerPath))
|
||||
call := mgr.Call(nmAgentManagerIface+".Register", 0, sa.id)
|
||||
if call.Err != nil {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("failed to register agent with NetworkManager: %w", call.Err)
|
||||
}
|
||||
|
||||
log.Infof("[SecretAgent] Registered with NetworkManager (id=%s, unique name=%s, fixed path=%s)", sa.id, c.Names()[0], sa.objPath)
|
||||
return sa, nil
|
||||
}
|
||||
|
||||
func (a *SecretAgent) Close() {
|
||||
if a.conn != nil {
|
||||
mgr := a.conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath(nmAgentManagerPath))
|
||||
mgr.Call(nmAgentManagerIface+".Unregister", 0, a.id)
|
||||
a.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *SecretAgent) GetSecrets(
|
||||
conn map[string]nmVariantMap,
|
||||
path dbus.ObjectPath,
|
||||
settingName string,
|
||||
hints []string,
|
||||
flags uint32,
|
||||
) (nmSettingMap, *dbus.Error) {
|
||||
log.Infof("[SecretAgent] GetSecrets called: path=%s, setting=%s, hints=%v, flags=%d",
|
||||
path, settingName, hints, flags)
|
||||
|
||||
const (
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION = 0x1
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW = 0x2
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED = 0x4
|
||||
)
|
||||
|
||||
connType, displayName, vpnSvc := readConnTypeAndName(conn)
|
||||
ssid := readSSID(conn)
|
||||
fields := fieldsNeeded(settingName, hints)
|
||||
|
||||
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d", connType, displayName, vpnSvc, fields, flags)
|
||||
|
||||
if a.backend != nil {
|
||||
a.backend.stateMutex.RLock()
|
||||
isConnecting := a.backend.state.IsConnecting
|
||||
connectingSSID := a.backend.state.ConnectingSSID
|
||||
isConnectingVPN := a.backend.state.IsConnectingVPN
|
||||
connectingVPNUUID := a.backend.state.ConnectingVPNUUID
|
||||
a.backend.stateMutex.RUnlock()
|
||||
|
||||
switch connType {
|
||||
case "802-11-wireless":
|
||||
// If we're connecting to a WiFi network, only respond if it's the one we're connecting to
|
||||
if isConnecting && connectingSSID != ssid {
|
||||
log.Infof("[SecretAgent] Ignoring WiFi request for SSID '%s' - we're connecting to '%s'", ssid, connectingSSID)
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||
}
|
||||
case "vpn", "wireguard":
|
||||
var connUuid string
|
||||
if c, ok := conn["connection"]; ok {
|
||||
if v, ok := c["uuid"]; ok {
|
||||
if s, ok2 := v.Value().(string); ok2 {
|
||||
connUuid = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're connecting to a VPN, only respond if it's the one we're connecting to
|
||||
// This prevents interfering with nmcli/other tools when our app isn't connecting
|
||||
if isConnectingVPN && connUuid != connectingVPNUUID {
|
||||
log.Infof("[SecretAgent] Ignoring VPN request for UUID '%s' - we're connecting to '%s'", connUuid, connectingVPNUUID)
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
// For VPN connections with no hints, we can't provide a proper UI.
|
||||
// Defer to other agents (like nm-applet or VPN-specific auth dialogs)
|
||||
// that can handle the VPN type properly (e.g., OpenConnect with SAML, etc.)
|
||||
if settingName == "vpn" {
|
||||
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||
}
|
||||
|
||||
const (
|
||||
NM_SETTING_SECRET_FLAG_NONE = 0
|
||||
NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1
|
||||
NM_SETTING_SECRET_FLAG_NOT_SAVED = 2
|
||||
NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4
|
||||
)
|
||||
|
||||
var passwordFlags uint32 = 0xFFFF
|
||||
switch settingName {
|
||||
case "802-11-wireless-security":
|
||||
if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok {
|
||||
if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok {
|
||||
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
|
||||
passwordFlags = pwdFlags
|
||||
}
|
||||
}
|
||||
}
|
||||
case "802-1x":
|
||||
if dot1xSettings, ok := conn["802-1x"]; ok {
|
||||
if flagsVariant, ok := dot1xSettings["password-flags"]; ok {
|
||||
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
|
||||
passwordFlags = pwdFlags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if passwordFlags == 0xFFFF {
|
||||
log.Warnf("[SecretAgent] Could not determine password-flags for empty hints - returning NoSecrets error")
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||
} else if passwordFlags&NM_SETTING_SECRET_FLAG_NOT_REQUIRED != 0 {
|
||||
log.Infof("[SecretAgent] Secrets not required (flags=%d)", passwordFlags)
|
||||
out := nmSettingMap{}
|
||||
out[settingName] = nmVariantMap{}
|
||||
return out, nil
|
||||
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
|
||||
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags)
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||
} else {
|
||||
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
|
||||
out := nmSettingMap{}
|
||||
out[settingName] = nmVariantMap{}
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
|
||||
reason := reasonFromFlags(flags)
|
||||
if a.manager != nil && connType == "802-11-wireless" && a.manager.WasRecentlyFailed(ssid) {
|
||||
reason = "wrong-password"
|
||||
}
|
||||
|
||||
var connId, connUuid string
|
||||
if c, ok := conn["connection"]; ok {
|
||||
if v, ok := c["id"]; ok {
|
||||
if s, ok2 := v.Value().(string); ok2 {
|
||||
connId = s
|
||||
}
|
||||
}
|
||||
if v, ok := c["uuid"]; ok {
|
||||
if s, ok2 := v.Value().(string); ok2 {
|
||||
connUuid = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
token, err := a.prompts.Ask(ctx, PromptRequest{
|
||||
Name: displayName,
|
||||
SSID: ssid,
|
||||
ConnType: connType,
|
||||
VpnService: vpnSvc,
|
||||
SettingName: settingName,
|
||||
Fields: fields,
|
||||
Hints: hints,
|
||||
Reason: reason,
|
||||
ConnectionId: connId,
|
||||
ConnectionUuid: connUuid,
|
||||
ConnectionPath: string(path),
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("[SecretAgent] Failed to create prompt: %v", err)
|
||||
return nil, dbus.MakeFailedError(err)
|
||||
}
|
||||
|
||||
log.Infof("[SecretAgent] Waiting for user input (token=%s)", token)
|
||||
reply, err := a.prompts.Wait(ctx, token)
|
||||
if err != nil {
|
||||
log.Warnf("[SecretAgent] Prompt failed or cancelled: %v", err)
|
||||
|
||||
// Clear connecting state immediately on cancellation
|
||||
if a.backend != nil {
|
||||
a.backend.stateMutex.Lock()
|
||||
wasConnecting := a.backend.state.IsConnecting
|
||||
wasConnectingVPN := a.backend.state.IsConnectingVPN
|
||||
cancelledSSID := a.backend.state.ConnectingSSID
|
||||
if wasConnecting || wasConnectingVPN {
|
||||
log.Infof("[SecretAgent] Clearing connecting state due to cancelled prompt")
|
||||
a.backend.state.IsConnecting = false
|
||||
a.backend.state.ConnectingSSID = ""
|
||||
a.backend.state.IsConnectingVPN = false
|
||||
a.backend.state.ConnectingVPNUUID = ""
|
||||
}
|
||||
a.backend.stateMutex.Unlock()
|
||||
|
||||
// If this was a WiFi connection that was just cancelled, remove the connection profile
|
||||
// (it was created with AddConnection but activation was cancelled)
|
||||
if wasConnecting && cancelledSSID != "" && connType == "802-11-wireless" {
|
||||
log.Infof("[SecretAgent] Removing connection profile for cancelled WiFi connection: %s", cancelledSSID)
|
||||
if err := a.backend.ForgetWiFiNetwork(cancelledSSID); err != nil {
|
||||
log.Warnf("[SecretAgent] Failed to remove cancelled connection profile: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if (wasConnecting || wasConnectingVPN) && a.backend.onStateChange != nil {
|
||||
a.backend.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
if reply.Cancel || errors.Is(err, errdefs.ErrSecretPromptCancelled) {
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.UserCanceled", nil)
|
||||
}
|
||||
|
||||
if errors.Is(err, errdefs.ErrSecretPromptTimeout) {
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.Failed", nil)
|
||||
}
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.Failed", nil)
|
||||
}
|
||||
|
||||
log.Infof("[SecretAgent] User provided secrets, save=%v", reply.Save)
|
||||
|
||||
out := nmSettingMap{}
|
||||
sec := nmVariantMap{}
|
||||
for k, v := range reply.Secrets {
|
||||
sec[k] = dbus.MakeVariant(v)
|
||||
}
|
||||
out[settingName] = sec
|
||||
|
||||
switch settingName {
|
||||
case "802-1x":
|
||||
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec))
|
||||
case "vpn":
|
||||
log.Infof("[SecretAgent] Returning VPN secrets with %d fields for %s", len(sec), vpnSvc)
|
||||
}
|
||||
|
||||
// If save=true, persist secrets in background after returning to NetworkManager
|
||||
// This MUST happen after we return secrets, in a goroutine
|
||||
if reply.Save {
|
||||
go func() {
|
||||
log.Infof("[SecretAgent] Persisting secrets with Update2: path=%s, setting=%s", path, settingName)
|
||||
|
||||
// Get existing connection settings
|
||||
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
|
||||
var existingSettings map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
|
||||
log.Warnf("[SecretAgent] GetSettings failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build minimal settings with ONLY the section we're updating
|
||||
// This avoids D-Bus type serialization issues with complex types like IPv6 addresses
|
||||
settings := make(map[string]map[string]dbus.Variant)
|
||||
|
||||
// Copy connection section (required for Update2)
|
||||
if connSection, ok := existingSettings["connection"]; ok {
|
||||
settings["connection"] = connSection
|
||||
}
|
||||
|
||||
// Update settings based on type
|
||||
switch settingName {
|
||||
case "vpn":
|
||||
// Set password-flags=0 and add secrets to vpn section
|
||||
vpn, ok := existingSettings["vpn"]
|
||||
if !ok {
|
||||
vpn = make(map[string]dbus.Variant)
|
||||
}
|
||||
|
||||
// Get existing data map (vpn.data is string->string)
|
||||
var data map[string]string
|
||||
if dataVariant, ok := vpn["data"]; ok {
|
||||
if dm, ok := dataVariant.Value().(map[string]string); ok {
|
||||
data = make(map[string]string)
|
||||
for k, v := range dm {
|
||||
data[k] = v
|
||||
}
|
||||
} else {
|
||||
data = make(map[string]string)
|
||||
}
|
||||
} else {
|
||||
data = make(map[string]string)
|
||||
}
|
||||
|
||||
// Update password-flags to 0 (system-stored)
|
||||
data["password-flags"] = "0"
|
||||
vpn["data"] = dbus.MakeVariant(data)
|
||||
|
||||
// Add secrets (vpn.secrets is string->string)
|
||||
secs := make(map[string]string)
|
||||
for k, v := range reply.Secrets {
|
||||
secs[k] = v
|
||||
}
|
||||
vpn["secrets"] = dbus.MakeVariant(secs)
|
||||
settings["vpn"] = vpn
|
||||
|
||||
log.Infof("[SecretAgent] Updated VPN settings: password-flags=0, secrets with %d fields", len(secs))
|
||||
|
||||
case "802-11-wireless-security":
|
||||
// Set psk-flags=0 for WiFi
|
||||
wifiSec, ok := existingSettings["802-11-wireless-security"]
|
||||
if !ok {
|
||||
wifiSec = make(map[string]dbus.Variant)
|
||||
}
|
||||
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
|
||||
|
||||
// Add PSK secret
|
||||
if psk, ok := reply.Secrets["psk"]; ok {
|
||||
wifiSec["psk"] = dbus.MakeVariant(psk)
|
||||
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
|
||||
}
|
||||
settings["802-11-wireless-security"] = wifiSec
|
||||
|
||||
case "802-1x":
|
||||
// Set password-flags=0 for 802.1x
|
||||
dot1x, ok := existingSettings["802-1x"]
|
||||
if !ok {
|
||||
dot1x = make(map[string]dbus.Variant)
|
||||
}
|
||||
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
|
||||
|
||||
// Add password secret
|
||||
if password, ok := reply.Secrets["password"]; ok {
|
||||
dot1x["password"] = dbus.MakeVariant(password)
|
||||
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
|
||||
}
|
||||
settings["802-1x"] = dot1x
|
||||
}
|
||||
|
||||
// Call Update2 with correct signature:
|
||||
// Update2(IN settings, IN flags, IN args) -> OUT result
|
||||
// flags: 0x1 = to-disk
|
||||
var result map[string]dbus.Variant
|
||||
err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
||||
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result)
|
||||
if err != nil {
|
||||
log.Warnf("[SecretAgent] Update2(to-disk) failed: %v", err)
|
||||
} else {
|
||||
log.Infof("[SecretAgent] Successfully persisted secrets to disk for %s", settingName)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *SecretAgent) DeleteSecrets(conn map[string]nmVariantMap, path dbus.ObjectPath) *dbus.Error {
|
||||
ssid := readSSID(conn)
|
||||
log.Infof("[SecretAgent] DeleteSecrets called: path=%s, SSID=%s", path, ssid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *SecretAgent) DeleteSecrets2(path dbus.ObjectPath, setting string) *dbus.Error {
|
||||
log.Infof("[SecretAgent] DeleteSecrets2 (alternate) called: path=%s, setting=%s", path, setting)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *SecretAgent) CancelGetSecrets(path dbus.ObjectPath, settingName string) *dbus.Error {
|
||||
log.Infof("[SecretAgent] CancelGetSecrets called: path=%s, setting=%s", path, settingName)
|
||||
|
||||
if a.prompts != nil {
|
||||
if err := a.prompts.Cancel(string(path), settingName); err != nil {
|
||||
log.Warnf("[SecretAgent] Failed to cancel prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *SecretAgent) Introspect() (string, *dbus.Error) {
|
||||
return introspectXML, nil
|
||||
}
|
||||
|
||||
func readSSID(conn map[string]nmVariantMap) string {
|
||||
if w, ok := conn["802-11-wireless"]; ok {
|
||||
if v, ok := w["ssid"]; ok {
|
||||
if b, ok := v.Value().([]byte); ok {
|
||||
return string(b)
|
||||
}
|
||||
if s, ok := v.Value().(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func readConnTypeAndName(conn map[string]nmVariantMap) (string, string, string) {
|
||||
var connType, name, svc string
|
||||
if c, ok := conn["connection"]; ok {
|
||||
if v, ok := c["type"]; ok {
|
||||
if s, ok2 := v.Value().(string); ok2 {
|
||||
connType = s
|
||||
}
|
||||
}
|
||||
if v, ok := c["id"]; ok {
|
||||
if s, ok2 := v.Value().(string); ok2 {
|
||||
name = s
|
||||
}
|
||||
}
|
||||
}
|
||||
if vpn, ok := conn["vpn"]; ok {
|
||||
if v, ok := vpn["service-type"]; ok {
|
||||
if s, ok2 := v.Value().(string); ok2 {
|
||||
svc = s
|
||||
}
|
||||
}
|
||||
}
|
||||
if name == "" && connType == "802-11-wireless" {
|
||||
name = readSSID(conn)
|
||||
}
|
||||
return connType, name, svc
|
||||
}
|
||||
|
||||
func fieldsNeeded(setting string, hints []string) []string {
|
||||
switch setting {
|
||||
case "802-11-wireless-security":
|
||||
return []string{"psk"}
|
||||
case "802-1x":
|
||||
return []string{"identity", "password"}
|
||||
case "vpn":
|
||||
return hints
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func reasonFromFlags(flags uint32) string {
|
||||
const (
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION = 0x1
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW = 0x2
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED = 0x4
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_WPS_PBC_ACTIVE = 0x8
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_ONLY_SYSTEM = 0x80000000
|
||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_NO_ERRORS = 0x40000000
|
||||
)
|
||||
|
||||
if flags&NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW != 0 {
|
||||
return "wrong-password"
|
||||
}
|
||||
if flags&NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED != 0 {
|
||||
return "user-requested"
|
||||
}
|
||||
return "required"
|
||||
}
|
||||
65
core/internal/server/network/backend.go
Normal file
65
core/internal/server/network/backend.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package network
|
||||
|
||||
type Backend interface {
|
||||
Initialize() error
|
||||
Close()
|
||||
|
||||
GetWiFiEnabled() (bool, error)
|
||||
SetWiFiEnabled(enabled bool) error
|
||||
|
||||
ScanWiFi() error
|
||||
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
|
||||
|
||||
ConnectWiFi(req ConnectionRequest) error
|
||||
DisconnectWiFi() error
|
||||
ForgetWiFiNetwork(ssid string) error
|
||||
SetWiFiAutoconnect(ssid string, autoconnect bool) error
|
||||
|
||||
GetWiredConnections() ([]WiredConnection, error)
|
||||
GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error)
|
||||
ConnectEthernet() error
|
||||
DisconnectEthernet() error
|
||||
ActivateWiredConnection(uuid string) error
|
||||
|
||||
ListVPNProfiles() ([]VPNProfile, error)
|
||||
ListActiveVPN() ([]VPNActive, error)
|
||||
ConnectVPN(uuidOrName string, singleActive bool) error
|
||||
DisconnectVPN(uuidOrName string) error
|
||||
DisconnectAllVPN() error
|
||||
ClearVPNCredentials(uuidOrName string) error
|
||||
|
||||
GetCurrentState() (*BackendState, error)
|
||||
|
||||
StartMonitoring(onStateChange func()) error
|
||||
StopMonitoring()
|
||||
|
||||
GetPromptBroker() PromptBroker
|
||||
SetPromptBroker(broker PromptBroker) error
|
||||
SubmitCredentials(token string, secrets map[string]string, save bool) error
|
||||
CancelCredentials(token string) error
|
||||
}
|
||||
|
||||
type BackendState struct {
|
||||
Backend string
|
||||
NetworkStatus NetworkStatus
|
||||
EthernetIP string
|
||||
EthernetDevice string
|
||||
EthernetConnected bool
|
||||
EthernetConnectionUuid string
|
||||
WiFiIP string
|
||||
WiFiDevice string
|
||||
WiFiConnected bool
|
||||
WiFiEnabled bool
|
||||
WiFiSSID string
|
||||
WiFiBSSID string
|
||||
WiFiSignal uint8
|
||||
WiFiNetworks []WiFiNetwork
|
||||
WiredConnections []WiredConnection
|
||||
VPNProfiles []VPNProfile
|
||||
VPNActive []VPNActive
|
||||
IsConnecting bool
|
||||
ConnectingSSID string
|
||||
IsConnectingVPN bool
|
||||
ConnectingVPNUUID string
|
||||
LastError string
|
||||
}
|
||||
198
core/internal/server/network/backend_hybrid_iwd_networkd.go
Normal file
198
core/internal/server/network/backend_hybrid_iwd_networkd.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type HybridIwdNetworkdBackend struct {
|
||||
wifi *IWDBackend
|
||||
l3 *SystemdNetworkdBackend
|
||||
onStateChange func()
|
||||
stateMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewHybridIwdNetworkdBackend(w *IWDBackend, n *SystemdNetworkdBackend) (*HybridIwdNetworkdBackend, error) {
|
||||
return &HybridIwdNetworkdBackend{
|
||||
wifi: w,
|
||||
l3: n,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) Initialize() error {
|
||||
if err := b.wifi.Initialize(); err != nil {
|
||||
return fmt.Errorf("iwd init: %w", err)
|
||||
}
|
||||
if err := b.l3.Initialize(); err != nil {
|
||||
return fmt.Errorf("networkd init: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) Close() {
|
||||
b.wifi.Close()
|
||||
b.l3.Close()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) StartMonitoring(onStateChange func()) error {
|
||||
b.onStateChange = onStateChange
|
||||
|
||||
mergedCallback := func() {
|
||||
ws, _ := b.wifi.GetCurrentState()
|
||||
ls, _ := b.l3.GetCurrentState()
|
||||
|
||||
if ws != nil && ls != nil && ws.WiFiDevice != "" && ls.WiFiIP != "" {
|
||||
b.wifi.MarkIPConfigSeen()
|
||||
}
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.wifi.StartMonitoring(mergedCallback); err != nil {
|
||||
return fmt.Errorf("wifi monitoring: %w", err)
|
||||
}
|
||||
if err := b.l3.StartMonitoring(mergedCallback); err != nil {
|
||||
return fmt.Errorf("l3 monitoring: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) StopMonitoring() {
|
||||
b.wifi.StopMonitoring()
|
||||
b.l3.StopMonitoring()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetCurrentState() (*BackendState, error) {
|
||||
ws, err := b.wifi.GetCurrentState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls, err := b.l3.GetCurrentState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
merged := *ws
|
||||
merged.Backend = "iwd+networkd"
|
||||
|
||||
merged.WiFiIP = ls.WiFiIP
|
||||
merged.EthernetConnected = ls.EthernetConnected
|
||||
merged.EthernetIP = ls.EthernetIP
|
||||
merged.EthernetDevice = ls.EthernetDevice
|
||||
merged.EthernetConnectionUuid = ls.EthernetConnectionUuid
|
||||
merged.WiredConnections = ls.WiredConnections
|
||||
|
||||
if ls.EthernetConnected && ls.EthernetIP != "" {
|
||||
merged.NetworkStatus = StatusEthernet
|
||||
} else if ws.WiFiConnected && ls.WiFiIP != "" {
|
||||
merged.NetworkStatus = StatusWiFi
|
||||
} else {
|
||||
merged.NetworkStatus = StatusDisconnected
|
||||
}
|
||||
|
||||
return &merged, nil
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetWiFiEnabled() (bool, error) {
|
||||
return b.wifi.GetWiFiEnabled()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) SetWiFiEnabled(enabled bool) error {
|
||||
return b.wifi.SetWiFiEnabled(enabled)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ScanWiFi() error {
|
||||
return b.wifi.ScanWiFi()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) {
|
||||
return b.wifi.GetWiFiNetworkDetails(ssid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
if err := b.wifi.ConnectWiFi(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws, err := b.wifi.GetCurrentState()
|
||||
if err == nil && ws.WiFiDevice != "" {
|
||||
b.l3.EnsureDhcpUp(ws.WiFiDevice)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) DisconnectWiFi() error {
|
||||
return b.wifi.DisconnectWiFi()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ForgetWiFiNetwork(ssid string) error {
|
||||
return b.wifi.ForgetWiFiNetwork(ssid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
return b.l3.GetWiredConnections()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error) {
|
||||
return b.l3.GetWiredNetworkDetails(uuid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ConnectEthernet() error {
|
||||
return b.l3.ConnectEthernet()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) DisconnectEthernet() error {
|
||||
return b.l3.DisconnectEthernet()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ActivateWiredConnection(uuid string) error {
|
||||
return b.l3.ActivateWiredConnection(uuid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
||||
return []VPNProfile{}, nil
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ListActiveVPN() ([]VPNActive, error) {
|
||||
return []VPNActive{}, nil
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||
return fmt.Errorf("VPN not supported in hybrid mode")
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) DisconnectVPN(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported in hybrid mode")
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) DisconnectAllVPN() error {
|
||||
return fmt.Errorf("VPN not supported in hybrid mode")
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ClearVPNCredentials(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported in hybrid mode")
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetPromptBroker() PromptBroker {
|
||||
return b.wifi.GetPromptBroker()
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) SetPromptBroker(broker PromptBroker) error {
|
||||
return b.wifi.SetPromptBroker(broker)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error {
|
||||
return b.wifi.SubmitCredentials(token, secrets, save)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) CancelCredentials(token string) error {
|
||||
return b.wifi.CancelCredentials(token)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
|
||||
return b.wifi.SetWiFiAutoconnect(ssid, autoconnect)
|
||||
}
|
||||
135
core/internal/server/network/backend_hybrid_test.go
Normal file
135
core/internal/server/network/backend_hybrid_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHybridIwdNetworkdBackend_New(t *testing.T) {
|
||||
wifi, _ := NewIWDBackend()
|
||||
l3, _ := NewSystemdNetworkdBackend()
|
||||
|
||||
hybrid, err := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, hybrid)
|
||||
assert.NotNil(t, hybrid.wifi)
|
||||
assert.NotNil(t, hybrid.l3)
|
||||
}
|
||||
|
||||
func TestHybridIwdNetworkdBackend_GetCurrentState_MergesState(t *testing.T) {
|
||||
wifi, _ := NewIWDBackend()
|
||||
l3, _ := NewSystemdNetworkdBackend()
|
||||
hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
|
||||
wifi.state.WiFiConnected = true
|
||||
wifi.state.WiFiSSID = "TestNetwork"
|
||||
wifi.state.WiFiBSSID = "00:11:22:33:44:55"
|
||||
wifi.state.WiFiSignal = 75
|
||||
wifi.state.WiFiDevice = "wlan0"
|
||||
|
||||
l3.state.WiFiIP = "192.168.1.100"
|
||||
l3.state.EthernetConnected = false
|
||||
|
||||
state, err := hybrid.GetCurrentState()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, state)
|
||||
assert.Equal(t, "iwd+networkd", state.Backend)
|
||||
assert.Equal(t, "TestNetwork", state.WiFiSSID)
|
||||
assert.Equal(t, "00:11:22:33:44:55", state.WiFiBSSID)
|
||||
assert.Equal(t, uint8(75), state.WiFiSignal)
|
||||
assert.Equal(t, "192.168.1.100", state.WiFiIP)
|
||||
assert.True(t, state.WiFiConnected)
|
||||
assert.False(t, state.EthernetConnected)
|
||||
assert.Equal(t, StatusWiFi, state.NetworkStatus)
|
||||
}
|
||||
|
||||
func TestHybridIwdNetworkdBackend_GetCurrentState_EthernetPriority(t *testing.T) {
|
||||
wifi, _ := NewIWDBackend()
|
||||
l3, _ := NewSystemdNetworkdBackend()
|
||||
hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
|
||||
wifi.state.WiFiConnected = true
|
||||
wifi.state.WiFiSSID = "TestNetwork"
|
||||
|
||||
l3.state.WiFiIP = "192.168.1.100"
|
||||
l3.state.EthernetConnected = true
|
||||
l3.state.EthernetIP = "192.168.1.50"
|
||||
l3.state.EthernetDevice = "eth0"
|
||||
|
||||
state, err := hybrid.GetCurrentState()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, StatusEthernet, state.NetworkStatus)
|
||||
assert.Equal(t, "192.168.1.50", state.EthernetIP)
|
||||
assert.Equal(t, "eth0", state.EthernetDevice)
|
||||
}
|
||||
|
||||
func TestHybridIwdNetworkdBackend_GetCurrentState_WiFiNoIP(t *testing.T) {
|
||||
wifi, _ := NewIWDBackend()
|
||||
l3, _ := NewSystemdNetworkdBackend()
|
||||
hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
|
||||
wifi.state.WiFiConnected = true
|
||||
wifi.state.WiFiSSID = "TestNetwork"
|
||||
|
||||
l3.state.WiFiIP = ""
|
||||
l3.state.EthernetConnected = false
|
||||
|
||||
state, err := hybrid.GetCurrentState()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, StatusDisconnected, state.NetworkStatus)
|
||||
assert.True(t, state.WiFiConnected)
|
||||
assert.Empty(t, state.WiFiIP)
|
||||
}
|
||||
|
||||
func TestHybridIwdNetworkdBackend_WiFiDelegation(t *testing.T) {
|
||||
wifi, _ := NewIWDBackend()
|
||||
l3, _ := NewSystemdNetworkdBackend()
|
||||
hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
|
||||
enabled, err := hybrid.GetWiFiEnabled()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, enabled)
|
||||
|
||||
state, err := hybrid.GetCurrentState()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, state)
|
||||
assert.Equal(t, "iwd+networkd", state.Backend)
|
||||
}
|
||||
|
||||
func TestHybridIwdNetworkdBackend_WiredDelegation(t *testing.T) {
|
||||
wifi, _ := NewIWDBackend()
|
||||
l3, _ := NewSystemdNetworkdBackend()
|
||||
hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
|
||||
conns, err := hybrid.GetWiredConnections()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, conns)
|
||||
}
|
||||
|
||||
func TestHybridIwdNetworkdBackend_VPNNotSupported(t *testing.T) {
|
||||
wifi, _ := NewIWDBackend()
|
||||
l3, _ := NewSystemdNetworkdBackend()
|
||||
hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
|
||||
profiles, err := hybrid.ListVPNProfiles()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, profiles)
|
||||
|
||||
active, err := hybrid.ListActiveVPN()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, active)
|
||||
|
||||
err = hybrid.ConnectVPN("test", false)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
}
|
||||
|
||||
func TestHybridIwdNetworkdBackend_PromptBrokerDelegation(t *testing.T) {
|
||||
wifi, _ := NewIWDBackend()
|
||||
l3, _ := NewSystemdNetworkdBackend()
|
||||
hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
|
||||
broker := hybrid.GetPromptBroker()
|
||||
assert.Nil(t, broker)
|
||||
}
|
||||
232
core/internal/server/network/backend_iwd.go
Normal file
232
core/internal/server/network/backend_iwd.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
iwdBusName = "net.connman.iwd"
|
||||
iwdObjectPath = "/"
|
||||
iwdAdapterInterface = "net.connman.iwd.Adapter"
|
||||
iwdDeviceInterface = "net.connman.iwd.Device"
|
||||
iwdStationInterface = "net.connman.iwd.Station"
|
||||
iwdNetworkInterface = "net.connman.iwd.Network"
|
||||
iwdKnownNetworkInterface = "net.connman.iwd.KnownNetwork"
|
||||
dbusObjectManager = "org.freedesktop.DBus.ObjectManager"
|
||||
dbusPropertiesInterface = "org.freedesktop.DBus.Properties"
|
||||
)
|
||||
|
||||
type connectAttempt struct {
|
||||
ssid string
|
||||
netPath dbus.ObjectPath
|
||||
start time.Time
|
||||
deadline time.Time
|
||||
sawAuthish bool
|
||||
connectedAt time.Time
|
||||
sawIPConfig bool
|
||||
sawPromptRetry bool
|
||||
finalized bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type IWDBackend struct {
|
||||
conn *dbus.Conn
|
||||
state *BackendState
|
||||
stateMutex sync.RWMutex
|
||||
promptBroker PromptBroker
|
||||
onStateChange func()
|
||||
|
||||
devicePath dbus.ObjectPath
|
||||
stationPath dbus.ObjectPath
|
||||
adapterPath dbus.ObjectPath
|
||||
|
||||
iwdAgent *IWDAgent
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
curAttempt *connectAttempt
|
||||
attemptMutex sync.RWMutex
|
||||
recentScans map[string]time.Time
|
||||
recentScansMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewIWDBackend() (*IWDBackend, error) {
|
||||
backend := &IWDBackend{
|
||||
state: &BackendState{
|
||||
Backend: "iwd",
|
||||
WiFiEnabled: true,
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
recentScans: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) Initialize() error {
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to system bus: %w", err)
|
||||
}
|
||||
b.conn = conn
|
||||
|
||||
if err := b.discoverDevices(); err != nil {
|
||||
conn.Close()
|
||||
return fmt.Errorf("failed to discover iwd devices: %w", err)
|
||||
}
|
||||
|
||||
if err := b.updateState(); err != nil {
|
||||
conn.Close()
|
||||
return fmt.Errorf("failed to get initial state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) Close() {
|
||||
close(b.stopChan)
|
||||
b.sigWG.Wait()
|
||||
|
||||
if b.iwdAgent != nil {
|
||||
b.iwdAgent.Close()
|
||||
}
|
||||
|
||||
if b.conn != nil {
|
||||
b.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) discoverDevices() error {
|
||||
obj := b.conn.Object(iwdBusName, iwdObjectPath)
|
||||
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get managed objects: %w", err)
|
||||
}
|
||||
|
||||
for path, interfaces := range objects {
|
||||
if _, hasStation := interfaces[iwdStationInterface]; hasStation {
|
||||
b.stationPath = path
|
||||
}
|
||||
if _, hasDevice := interfaces[iwdDeviceInterface]; hasDevice {
|
||||
b.devicePath = path
|
||||
|
||||
if devProps, ok := interfaces[iwdDeviceInterface]; ok {
|
||||
if nameVar, ok := devProps["Name"]; ok {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiDevice = name
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter {
|
||||
b.adapterPath = path
|
||||
}
|
||||
}
|
||||
|
||||
if b.stationPath == "" || b.devicePath == "" {
|
||||
return fmt.Errorf("no WiFi device found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetCurrentState() (*BackendState, error) {
|
||||
state := *b.state
|
||||
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
|
||||
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) OnUserCanceledPrompt() {
|
||||
b.stateMutex.RLock()
|
||||
cancelledSSID := b.state.ConnectingSSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
b.setConnectError("user-canceled")
|
||||
|
||||
if cancelledSSID != "" {
|
||||
if err := b.ForgetWiFiNetwork(cancelledSSID); err != nil {
|
||||
}
|
||||
}
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) OnPromptRetry(ssid string) {
|
||||
b.attemptMutex.RLock()
|
||||
att := b.curAttempt
|
||||
b.attemptMutex.RUnlock()
|
||||
|
||||
if att != nil && att.ssid == ssid {
|
||||
att.mu.Lock()
|
||||
att.sawPromptRetry = true
|
||||
att.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) MarkIPConfigSeen() {
|
||||
b.attemptMutex.RLock()
|
||||
att := b.curAttempt
|
||||
b.attemptMutex.RUnlock()
|
||||
if att != nil {
|
||||
att.mu.Lock()
|
||||
att.sawIPConfig = true
|
||||
att.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetPromptBroker() PromptBroker {
|
||||
return b.promptBroker
|
||||
}
|
||||
|
||||
func (b *IWDBackend) SetPromptBroker(broker PromptBroker) error {
|
||||
if broker == nil {
|
||||
return fmt.Errorf("broker cannot be nil")
|
||||
}
|
||||
|
||||
b.promptBroker = broker
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) 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 *IWDBackend) 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 *IWDBackend) StopMonitoring() {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
default:
|
||||
close(b.stopChan)
|
||||
}
|
||||
b.sigWG.Wait()
|
||||
}
|
||||
355
core/internal/server/network/backend_iwd_signals.go
Normal file
355
core/internal/server/network/backend_iwd_signals.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func (b *IWDBackend) StartMonitoring(onStateChange func()) error {
|
||||
b.onStateChange = onStateChange
|
||||
|
||||
if b.promptBroker != nil {
|
||||
agent, err := NewIWDAgent(b.conn, b.promptBroker)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start IWD agent: %w", err)
|
||||
}
|
||||
agent.onUserCanceled = b.OnUserCanceledPrompt
|
||||
agent.onPromptRetry = b.OnPromptRetry
|
||||
b.iwdAgent = agent
|
||||
}
|
||||
|
||||
sigChan := make(chan *dbus.Signal, 100)
|
||||
b.conn.Signal(sigChan)
|
||||
|
||||
if b.devicePath != "" {
|
||||
err := b.conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(b.devicePath),
|
||||
dbus.WithMatchInterface(dbusPropertiesInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add device signal match: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if b.stationPath != "" {
|
||||
err := b.conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(b.stationPath),
|
||||
dbus.WithMatchInterface(dbusPropertiesInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add station signal match: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.sigWG.Add(1)
|
||||
go b.signalHandler(sigChan)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
|
||||
defer b.sigWG.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
b.conn.RemoveSignal(sigChan)
|
||||
close(sigChan)
|
||||
return
|
||||
|
||||
case sig := <-sigChan:
|
||||
if sig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if sig.Name != dbusPropertiesInterface+".PropertiesChanged" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(sig.Body) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
iface, ok := sig.Body[0].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
changed, ok := sig.Body[1].(map[string]dbus.Variant)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
stateChanged := false
|
||||
|
||||
switch iface {
|
||||
case iwdDeviceInterface:
|
||||
if sig.Path == b.devicePath {
|
||||
if poweredVar, ok := changed["Powered"]; ok {
|
||||
if powered, ok := poweredVar.Value().(bool); ok {
|
||||
b.stateMutex.Lock()
|
||||
if b.state.WiFiEnabled != powered {
|
||||
b.state.WiFiEnabled = powered
|
||||
stateChanged = true
|
||||
}
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case iwdStationInterface:
|
||||
if sig.Path == b.stationPath {
|
||||
if scanningVar, ok := changed["Scanning"]; ok {
|
||||
if scanning, ok := scanningVar.Value().(bool); ok && !scanning {
|
||||
networks, err := b.updateWiFiNetworks()
|
||||
if err == nil {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiNetworks = networks
|
||||
b.stateMutex.Unlock()
|
||||
stateChanged = true
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
wifiConnected := b.state.WiFiConnected
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
if wifiConnected {
|
||||
stationObj := b.conn.Object(iwdBusName, b.stationPath)
|
||||
connNetVar, err := stationObj.GetProperty(iwdStationInterface + ".ConnectedNetwork")
|
||||
if err == nil && connNetVar.Value() != nil {
|
||||
if netPath, ok := connNetVar.Value().(dbus.ObjectPath); ok && netPath != "/" {
|
||||
var orderedNetworks [][]dbus.Variant
|
||||
err = stationObj.Call(iwdStationInterface+".GetOrderedNetworks", 0).Store(&orderedNetworks)
|
||||
if err == nil {
|
||||
for _, netData := range orderedNetworks {
|
||||
if len(netData) < 2 {
|
||||
continue
|
||||
}
|
||||
currentNetPath, ok := netData[0].Value().(dbus.ObjectPath)
|
||||
if !ok || currentNetPath != netPath {
|
||||
continue
|
||||
}
|
||||
signalStrength, ok := netData[1].Value().(int16)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
signalDbm := signalStrength / 100
|
||||
signal := uint8(signalDbm + 100)
|
||||
if signalDbm > 0 {
|
||||
signal = 100
|
||||
} else if signalDbm < -100 {
|
||||
signal = 0
|
||||
}
|
||||
b.stateMutex.Lock()
|
||||
if b.state.WiFiSignal != signal {
|
||||
b.state.WiFiSignal = signal
|
||||
stateChanged = true
|
||||
}
|
||||
b.stateMutex.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stateVar, ok := changed["State"]; ok {
|
||||
if state, ok := stateVar.Value().(string); ok {
|
||||
b.attemptMutex.RLock()
|
||||
att := b.curAttempt
|
||||
b.attemptMutex.RUnlock()
|
||||
|
||||
var connPath dbus.ObjectPath
|
||||
if v, ok := changed["ConnectedNetwork"]; ok {
|
||||
if v.Value() != nil {
|
||||
if p, ok := v.Value().(dbus.ObjectPath); ok {
|
||||
connPath = p
|
||||
}
|
||||
}
|
||||
}
|
||||
if connPath == "" {
|
||||
station := b.conn.Object(iwdBusName, b.stationPath)
|
||||
if cnVar, err := station.GetProperty(iwdStationInterface + ".ConnectedNetwork"); err == nil && cnVar.Value() != nil {
|
||||
cnVar.Store(&connPath)
|
||||
}
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
prevConnected := b.state.WiFiConnected
|
||||
prevSSID := b.state.WiFiSSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
targetPath := dbus.ObjectPath("")
|
||||
if att != nil {
|
||||
targetPath = att.netPath
|
||||
}
|
||||
|
||||
isTarget := att != nil && targetPath != "" && connPath == targetPath
|
||||
|
||||
if att != nil {
|
||||
switch state {
|
||||
case "authenticating", "associating", "associated", "roaming":
|
||||
att.mu.Lock()
|
||||
att.sawAuthish = true
|
||||
att.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
if att != nil && state == "connected" && isTarget {
|
||||
att.mu.Lock()
|
||||
if att.connectedAt.IsZero() {
|
||||
att.connectedAt = time.Now()
|
||||
}
|
||||
att.mu.Unlock()
|
||||
}
|
||||
|
||||
if att != nil && state == "configuring" {
|
||||
att.mu.Lock()
|
||||
att.sawIPConfig = true
|
||||
att.mu.Unlock()
|
||||
}
|
||||
|
||||
switch state {
|
||||
case "connected":
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiConnected = true
|
||||
b.state.NetworkStatus = StatusWiFi
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = ""
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if connPath != "" && connPath != "/" {
|
||||
netObj := b.conn.Object(iwdBusName, connPath)
|
||||
if nameVar, err := netObj.GetProperty(iwdNetworkInterface + ".Name"); err == nil {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiSSID = name
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stateChanged = true
|
||||
|
||||
if att != nil && isTarget {
|
||||
go func(attLocal *connectAttempt, tgt dbus.ObjectPath) {
|
||||
time.Sleep(3 * time.Second)
|
||||
station := b.conn.Object(iwdBusName, b.stationPath)
|
||||
var nowState string
|
||||
if stVar, err := station.GetProperty(iwdStationInterface + ".State"); err == nil {
|
||||
stVar.Store(&nowState)
|
||||
}
|
||||
var nowConn dbus.ObjectPath
|
||||
if cnVar, err := station.GetProperty(iwdStationInterface + ".ConnectedNetwork"); err == nil && cnVar.Value() != nil {
|
||||
cnVar.Store(&nowConn)
|
||||
}
|
||||
|
||||
if nowState == "connected" && nowConn == tgt {
|
||||
b.finalizeAttempt(attLocal, "")
|
||||
b.attemptMutex.Lock()
|
||||
if b.curAttempt == attLocal {
|
||||
b.curAttempt = nil
|
||||
}
|
||||
b.attemptMutex.Unlock()
|
||||
}
|
||||
}(att, targetPath)
|
||||
}
|
||||
|
||||
case "disconnecting", "disconnected":
|
||||
if att != nil {
|
||||
wasConnectedToTarget := prevConnected && prevSSID == att.ssid
|
||||
if wasConnectedToTarget || isTarget {
|
||||
code := b.classifyAttempt(att)
|
||||
b.finalizeAttempt(att, code)
|
||||
b.attemptMutex.Lock()
|
||||
if b.curAttempt == att {
|
||||
b.curAttempt = nil
|
||||
}
|
||||
b.attemptMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiConnected = false
|
||||
if state == "disconnected" {
|
||||
b.state.NetworkStatus = StatusDisconnected
|
||||
}
|
||||
b.stateMutex.Unlock()
|
||||
stateChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if connNetVar, ok := changed["ConnectedNetwork"]; ok {
|
||||
if netPath, ok := connNetVar.Value().(dbus.ObjectPath); ok && netPath != "/" {
|
||||
netObj := b.conn.Object(iwdBusName, netPath)
|
||||
nameVar, err := netObj.GetProperty(iwdNetworkInterface + ".Name")
|
||||
if err == nil {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
b.stateMutex.Lock()
|
||||
if b.state.WiFiSSID != name {
|
||||
b.state.WiFiSSID = name
|
||||
stateChanged = true
|
||||
}
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
stationObj := b.conn.Object(iwdBusName, b.stationPath)
|
||||
var orderedNetworks [][]dbus.Variant
|
||||
err = stationObj.Call(iwdStationInterface+".GetOrderedNetworks", 0).Store(&orderedNetworks)
|
||||
if err == nil {
|
||||
for _, netData := range orderedNetworks {
|
||||
if len(netData) < 2 {
|
||||
continue
|
||||
}
|
||||
currentNetPath, ok := netData[0].Value().(dbus.ObjectPath)
|
||||
if !ok || currentNetPath != netPath {
|
||||
continue
|
||||
}
|
||||
signalStrength, ok := netData[1].Value().(int16)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
signalDbm := signalStrength / 100
|
||||
signal := uint8(signalDbm + 100)
|
||||
if signalDbm > 0 {
|
||||
signal = 100
|
||||
} else if signalDbm < -100 {
|
||||
signal = 0
|
||||
}
|
||||
b.stateMutex.Lock()
|
||||
if b.state.WiFiSignal != signal {
|
||||
b.state.WiFiSignal = signal
|
||||
stateChanged = true
|
||||
}
|
||||
b.stateMutex.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b.stateMutex.Lock()
|
||||
if b.state.WiFiSSID != "" {
|
||||
b.state.WiFiSSID = ""
|
||||
b.state.WiFiSignal = 0
|
||||
stateChanged = true
|
||||
}
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stateChanged && b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
212
core/internal/server/network/backend_iwd_test.go
Normal file
212
core/internal/server/network/backend_iwd_test.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIWDBackend_MarkIPConfigSeen(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/net/connman/iwd/0/1/test",
|
||||
start: time.Now(),
|
||||
deadline: time.Now().Add(15 * time.Second),
|
||||
}
|
||||
|
||||
backend.attemptMutex.Lock()
|
||||
backend.curAttempt = att
|
||||
backend.attemptMutex.Unlock()
|
||||
|
||||
backend.MarkIPConfigSeen()
|
||||
|
||||
att.mu.Lock()
|
||||
assert.True(t, att.sawIPConfig, "sawIPConfig should be true after MarkIPConfigSeen")
|
||||
att.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestIWDBackend_MarkIPConfigSeen_NoAttempt(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
backend.attemptMutex.Lock()
|
||||
backend.curAttempt = nil
|
||||
backend.attemptMutex.Unlock()
|
||||
|
||||
backend.MarkIPConfigSeen()
|
||||
}
|
||||
|
||||
func TestIWDBackend_OnPromptRetry(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/net/connman/iwd/0/1/test",
|
||||
start: time.Now(),
|
||||
deadline: time.Now().Add(15 * time.Second),
|
||||
}
|
||||
|
||||
backend.attemptMutex.Lock()
|
||||
backend.curAttempt = att
|
||||
backend.attemptMutex.Unlock()
|
||||
|
||||
backend.OnPromptRetry("TestNetwork")
|
||||
|
||||
att.mu.Lock()
|
||||
assert.True(t, att.sawPromptRetry, "sawPromptRetry should be true after OnPromptRetry")
|
||||
att.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestIWDBackend_OnPromptRetry_WrongSSID(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/net/connman/iwd/0/1/test",
|
||||
start: time.Now(),
|
||||
deadline: time.Now().Add(15 * time.Second),
|
||||
}
|
||||
|
||||
backend.attemptMutex.Lock()
|
||||
backend.curAttempt = att
|
||||
backend.attemptMutex.Unlock()
|
||||
|
||||
backend.OnPromptRetry("DifferentNetwork")
|
||||
|
||||
att.mu.Lock()
|
||||
assert.False(t, att.sawPromptRetry, "sawPromptRetry should remain false for different SSID")
|
||||
att.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestIWDBackend_ClassifyAttempt_BadCredentials_PromptRetry(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/test",
|
||||
start: time.Now().Add(-5 * time.Second),
|
||||
deadline: time.Now().Add(10 * time.Second),
|
||||
sawPromptRetry: true,
|
||||
}
|
||||
|
||||
code := backend.classifyAttempt(att)
|
||||
assert.Equal(t, "bad-credentials", code)
|
||||
}
|
||||
|
||||
func TestIWDBackend_ClassifyAttempt_DhcpTimeout(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/test",
|
||||
start: time.Now().Add(-13 * time.Second),
|
||||
deadline: time.Now().Add(2 * time.Second),
|
||||
sawAuthish: true,
|
||||
sawIPConfig: false,
|
||||
}
|
||||
|
||||
code := backend.classifyAttempt(att)
|
||||
assert.Equal(t, "dhcp-timeout", code)
|
||||
}
|
||||
|
||||
func TestIWDBackend_ClassifyAttempt_AssocTimeout(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/test",
|
||||
start: time.Now().Add(-5 * time.Second),
|
||||
deadline: time.Now().Add(10 * time.Second),
|
||||
}
|
||||
|
||||
backend.recentScansMu.Lock()
|
||||
backend.recentScans["TestNetwork"] = time.Now()
|
||||
backend.recentScansMu.Unlock()
|
||||
|
||||
code := backend.classifyAttempt(att)
|
||||
assert.Equal(t, "assoc-timeout", code)
|
||||
}
|
||||
|
||||
func TestIWDBackend_ClassifyAttempt_NoSuchSSID(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/test",
|
||||
start: time.Now().Add(-5 * time.Second),
|
||||
deadline: time.Now().Add(10 * time.Second),
|
||||
}
|
||||
|
||||
code := backend.classifyAttempt(att)
|
||||
assert.Equal(t, "no-such-ssid", code)
|
||||
}
|
||||
|
||||
func TestIWDBackend_MapIwdDBusError(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"net.connman.iwd.Error.AlreadyConnected", "already-connected"},
|
||||
{"net.connman.iwd.Error.AuthenticationFailed", "bad-credentials"},
|
||||
{"net.connman.iwd.Error.InvalidKey", "bad-credentials"},
|
||||
{"net.connman.iwd.Error.IncorrectPassphrase", "bad-credentials"},
|
||||
{"net.connman.iwd.Error.NotFound", "no-such-ssid"},
|
||||
{"net.connman.iwd.Error.NotSupported", "connection-failed"},
|
||||
{"net.connman.iwd.Agent.Error.Canceled", "user-canceled"},
|
||||
{"net.connman.iwd.Error.Unknown", "connection-failed"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
code := backend.mapIwdDBusError(tc.name)
|
||||
assert.Equal(t, tc.expected, code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectAttempt_Finalization(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
backend.state = &BackendState{}
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/test",
|
||||
start: time.Now(),
|
||||
deadline: time.Now().Add(15 * time.Second),
|
||||
}
|
||||
|
||||
backend.finalizeAttempt(att, "bad-credentials")
|
||||
|
||||
att.mu.Lock()
|
||||
assert.True(t, att.finalized)
|
||||
att.mu.Unlock()
|
||||
|
||||
backend.stateMutex.RLock()
|
||||
assert.False(t, backend.state.IsConnecting)
|
||||
assert.Empty(t, backend.state.ConnectingSSID)
|
||||
assert.Equal(t, "bad-credentials", backend.state.LastError)
|
||||
backend.stateMutex.RUnlock()
|
||||
}
|
||||
|
||||
func TestConnectAttempt_DoubleFinalization(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
backend.state = &BackendState{}
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: "TestNetwork",
|
||||
netPath: "/test",
|
||||
start: time.Now(),
|
||||
deadline: time.Now().Add(15 * time.Second),
|
||||
}
|
||||
|
||||
backend.finalizeAttempt(att, "bad-credentials")
|
||||
backend.finalizeAttempt(att, "dhcp-timeout")
|
||||
|
||||
backend.stateMutex.RLock()
|
||||
assert.Equal(t, "bad-credentials", backend.state.LastError)
|
||||
backend.stateMutex.RUnlock()
|
||||
}
|
||||
47
core/internal/server/network/backend_iwd_unimplemented.go
Normal file
47
core/internal/server/network/backend_iwd_unimplemented.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package network
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
return nil, fmt.Errorf("wired connections not supported by iwd")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error) {
|
||||
return nil, fmt.Errorf("wired connections not supported by iwd")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ConnectEthernet() error {
|
||||
return fmt.Errorf("wired connections not supported by iwd")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) DisconnectEthernet() error {
|
||||
return fmt.Errorf("wired connections not supported by iwd")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ActivateWiredConnection(uuid string) error {
|
||||
return fmt.Errorf("wired connections not supported by iwd")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by iwd backend")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ListActiveVPN() ([]VPNActive, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by iwd backend")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||
return fmt.Errorf("VPN not supported by iwd backend")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) DisconnectVPN(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by iwd backend")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) DisconnectAllVPN() error {
|
||||
return fmt.Errorf("VPN not supported by iwd backend")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ClearVPNCredentials(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by iwd backend")
|
||||
}
|
||||
662
core/internal/server/network/backend_iwd_wifi.go
Normal file
662
core/internal/server/network/backend_iwd_wifi.go
Normal file
@@ -0,0 +1,662 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func (b *IWDBackend) updateState() error {
|
||||
if b.devicePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
obj := b.conn.Object(iwdBusName, b.devicePath)
|
||||
|
||||
poweredVar, err := obj.GetProperty(iwdDeviceInterface + ".Powered")
|
||||
if err == nil {
|
||||
if powered, ok := poweredVar.Value().(bool); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiEnabled = powered
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
if b.stationPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
stationObj := b.conn.Object(iwdBusName, b.stationPath)
|
||||
|
||||
stateVar, err := stationObj.GetProperty(iwdStationInterface + ".State")
|
||||
if err == nil {
|
||||
if state, ok := stateVar.Value().(string); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiConnected = (state == "connected")
|
||||
if state == "connected" {
|
||||
b.state.NetworkStatus = StatusWiFi
|
||||
} else {
|
||||
b.state.NetworkStatus = StatusDisconnected
|
||||
}
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
connNetVar, err := stationObj.GetProperty(iwdStationInterface + ".ConnectedNetwork")
|
||||
if err == nil && connNetVar.Value() != nil {
|
||||
if netPath, ok := connNetVar.Value().(dbus.ObjectPath); ok && netPath != "/" {
|
||||
netObj := b.conn.Object(iwdBusName, netPath)
|
||||
|
||||
nameVar, err := netObj.GetProperty(iwdNetworkInterface + ".Name")
|
||||
if err == nil {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiSSID = name
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var orderedNetworks [][]dbus.Variant
|
||||
err = stationObj.Call(iwdStationInterface+".GetOrderedNetworks", 0).Store(&orderedNetworks)
|
||||
if err == nil {
|
||||
for _, netData := range orderedNetworks {
|
||||
if len(netData) < 2 {
|
||||
continue
|
||||
}
|
||||
currentNetPath, ok := netData[0].Value().(dbus.ObjectPath)
|
||||
if !ok || currentNetPath != netPath {
|
||||
continue
|
||||
}
|
||||
signalStrength, ok := netData[1].Value().(int16)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
signalDbm := signalStrength / 100
|
||||
signal := uint8(signalDbm + 100)
|
||||
if signalDbm > 0 {
|
||||
signal = 100
|
||||
} else if signalDbm < -100 {
|
||||
signal = 0
|
||||
}
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiSignal = signal
|
||||
b.stateMutex.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
networks, err := b.updateWiFiNetworks()
|
||||
if err == nil {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiNetworks = networks
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetWiFiEnabled() (bool, error) {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
return b.state.WiFiEnabled, nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) SetWiFiEnabled(enabled bool) error {
|
||||
if b.devicePath == "" {
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
obj := b.conn.Object(iwdBusName, b.devicePath)
|
||||
call := obj.Call(dbusPropertiesInterface+".Set", 0, iwdDeviceInterface, "Powered", dbus.MakeVariant(enabled))
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to set WiFi enabled: %w", call.Err)
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiEnabled = enabled
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ScanWiFi() error {
|
||||
if b.stationPath == "" {
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
obj := b.conn.Object(iwdBusName, b.stationPath)
|
||||
|
||||
scanningVar, err := obj.GetProperty(iwdStationInterface + ".Scanning")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check scanning state: %w", err)
|
||||
}
|
||||
|
||||
if scanning, ok := scanningVar.Value().(bool); ok && scanning {
|
||||
return fmt.Errorf("scan already in progress")
|
||||
}
|
||||
|
||||
call := obj.Call(iwdStationInterface+".Scan", 0)
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("scan request failed: %w", call.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
||||
if b.stationPath == "" {
|
||||
return nil, fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
obj := b.conn.Object(iwdBusName, b.stationPath)
|
||||
|
||||
var orderedNetworks [][]dbus.Variant
|
||||
err := obj.Call(iwdStationInterface+".GetOrderedNetworks", 0).Store(&orderedNetworks)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get networks: %w", err)
|
||||
}
|
||||
|
||||
knownNetworks, err := b.getKnownNetworks()
|
||||
if err != nil {
|
||||
knownNetworks = make(map[string]bool)
|
||||
}
|
||||
|
||||
autoconnectMap, err := b.getAutoconnectSettings()
|
||||
if err != nil {
|
||||
autoconnectMap = make(map[string]bool)
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
currentSSID := b.state.WiFiSSID
|
||||
wifiConnected := b.state.WiFiConnected
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
networks := make([]WiFiNetwork, 0, len(orderedNetworks))
|
||||
for _, netData := range orderedNetworks {
|
||||
if len(netData) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
networkPath, ok := netData[0].Value().(dbus.ObjectPath)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
signalStrength, ok := netData[1].Value().(int16)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
netObj := b.conn.Object(iwdBusName, networkPath)
|
||||
|
||||
nameVar, err := netObj.GetProperty(iwdNetworkInterface + ".Name")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
name, ok := nameVar.Value().(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
typeVar, err := netObj.GetProperty(iwdNetworkInterface + ".Type")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
netType, ok := typeVar.Value().(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
signalDbm := signalStrength / 100
|
||||
signal := uint8(signalDbm + 100)
|
||||
if signalDbm > 0 {
|
||||
signal = 100
|
||||
} else if signalDbm < -100 {
|
||||
signal = 0
|
||||
}
|
||||
|
||||
secured := netType != "open"
|
||||
|
||||
network := WiFiNetwork{
|
||||
SSID: name,
|
||||
Signal: signal,
|
||||
Secured: secured,
|
||||
Connected: wifiConnected && name == currentSSID,
|
||||
Saved: knownNetworks[name],
|
||||
Autoconnect: autoconnectMap[name],
|
||||
Enterprise: netType == "8021x",
|
||||
}
|
||||
|
||||
networks = append(networks, network)
|
||||
}
|
||||
|
||||
sortWiFiNetworks(networks)
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiNetworks = networks
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
b.recentScansMu.Lock()
|
||||
for _, net := range networks {
|
||||
b.recentScans[net.SSID] = now
|
||||
}
|
||||
b.recentScansMu.Unlock()
|
||||
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) getKnownNetworks() (map[string]bool, error) {
|
||||
obj := b.conn.Object(iwdBusName, iwdObjectPath)
|
||||
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
known := make(map[string]bool)
|
||||
for _, interfaces := range objects {
|
||||
if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok {
|
||||
if nameVar, ok := knownProps["Name"]; ok {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
known[name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return known, nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) getAutoconnectSettings() (map[string]bool, error) {
|
||||
obj := b.conn.Object(iwdBusName, iwdObjectPath)
|
||||
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
autoconnectMap := make(map[string]bool)
|
||||
for _, interfaces := range objects {
|
||||
if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok {
|
||||
if nameVar, ok := knownProps["Name"]; ok {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
autoconnect := true
|
||||
if acVar, ok := knownProps["AutoConnect"]; ok {
|
||||
if ac, ok := acVar.Value().(bool); ok {
|
||||
autoconnect = ac
|
||||
}
|
||||
}
|
||||
autoconnectMap[name] = autoconnect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return autoconnectMap, nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) {
|
||||
b.stateMutex.RLock()
|
||||
networks := b.state.WiFiNetworks
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
var found *WiFiNetwork
|
||||
for i := range networks {
|
||||
if networks[i].SSID == ssid {
|
||||
found = &networks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("network not found: %s", ssid)
|
||||
}
|
||||
|
||||
return &NetworkInfoResponse{
|
||||
SSID: ssid,
|
||||
Bands: []WiFiNetwork{*found},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) setConnectError(code string) {
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = code
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (b *IWDBackend) 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 *IWDBackend) classifyAttempt(att *connectAttempt) string {
|
||||
att.mu.Lock()
|
||||
defer att.mu.Unlock()
|
||||
|
||||
if att.sawPromptRetry {
|
||||
return errdefs.ErrBadCredentials
|
||||
}
|
||||
|
||||
if !att.connectedAt.IsZero() && !att.sawIPConfig {
|
||||
connDuration := time.Since(att.connectedAt)
|
||||
if connDuration > 500*time.Millisecond && connDuration < 3*time.Second {
|
||||
return errdefs.ErrBadCredentials
|
||||
}
|
||||
}
|
||||
|
||||
if (att.sawAuthish || !att.connectedAt.IsZero()) && !att.sawIPConfig {
|
||||
if time.Since(att.start) > 12*time.Second {
|
||||
return errdefs.ErrDhcpTimeout
|
||||
}
|
||||
}
|
||||
|
||||
if !att.sawAuthish && att.connectedAt.IsZero() {
|
||||
if !b.seenInRecentScan(att.ssid) {
|
||||
return errdefs.ErrNoSuchSSID
|
||||
}
|
||||
return errdefs.ErrAssocTimeout
|
||||
}
|
||||
|
||||
return errdefs.ErrAssocTimeout
|
||||
}
|
||||
|
||||
func (b *IWDBackend) finalizeAttempt(att *connectAttempt, code string) {
|
||||
att.mu.Lock()
|
||||
if att.finalized {
|
||||
att.mu.Unlock()
|
||||
return
|
||||
}
|
||||
att.finalized = true
|
||||
att.mu.Unlock()
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = code
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
b.updateState()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) startAttemptWatchdog(att *connectAttempt) {
|
||||
b.sigWG.Add(1)
|
||||
go func() {
|
||||
defer b.sigWG.Done()
|
||||
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
att.mu.Lock()
|
||||
finalized := att.finalized
|
||||
att.mu.Unlock()
|
||||
|
||||
if finalized || time.Now().After(att.deadline) {
|
||||
if !finalized {
|
||||
b.finalizeAttempt(att, b.classifyAttempt(att))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
station := b.conn.Object(iwdBusName, b.stationPath)
|
||||
stVar, err := station.GetProperty(iwdStationInterface + ".State")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
state, _ := stVar.Value().(string)
|
||||
|
||||
cnVar, err := station.GetProperty(iwdStationInterface + ".ConnectedNetwork")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var connPath dbus.ObjectPath
|
||||
if cnVar.Value() != nil {
|
||||
connPath, _ = cnVar.Value().(dbus.ObjectPath)
|
||||
}
|
||||
|
||||
att.mu.Lock()
|
||||
if connPath == att.netPath && state == "connected" && att.connectedAt.IsZero() {
|
||||
att.connectedAt = time.Now()
|
||||
}
|
||||
if state == "configuring" {
|
||||
att.sawIPConfig = true
|
||||
}
|
||||
att.mu.Unlock()
|
||||
|
||||
case <-b.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *IWDBackend) mapIwdDBusError(name string) string {
|
||||
switch name {
|
||||
case "net.connman.iwd.Error.AlreadyConnected":
|
||||
return errdefs.ErrAlreadyConnected
|
||||
case "net.connman.iwd.Error.AuthenticationFailed",
|
||||
"net.connman.iwd.Error.InvalidKey",
|
||||
"net.connman.iwd.Error.IncorrectPassphrase":
|
||||
return errdefs.ErrBadCredentials
|
||||
case "net.connman.iwd.Error.NotFound":
|
||||
return errdefs.ErrNoSuchSSID
|
||||
case "net.connman.iwd.Error.NotSupported":
|
||||
return errdefs.ErrConnectionFailed
|
||||
case "net.connman.iwd.Agent.Error.Canceled":
|
||||
return errdefs.ErrUserCanceled
|
||||
default:
|
||||
return errdefs.ErrConnectionFailed
|
||||
}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
if b.stationPath == "" {
|
||||
b.setConnectError(errdefs.ErrWifiDisabled)
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
networkPath, err := b.findNetworkPath(req.SSID)
|
||||
if err != nil {
|
||||
b.setConnectError(errdefs.ErrNoSuchSSID)
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
return fmt.Errorf("network not found: %w", err)
|
||||
}
|
||||
|
||||
att := &connectAttempt{
|
||||
ssid: req.SSID,
|
||||
netPath: networkPath,
|
||||
start: time.Now(),
|
||||
deadline: time.Now().Add(15 * time.Second),
|
||||
}
|
||||
|
||||
b.attemptMutex.Lock()
|
||||
b.curAttempt = att
|
||||
b.attemptMutex.Unlock()
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnecting = true
|
||||
b.state.ConnectingSSID = req.SSID
|
||||
b.state.LastError = ""
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
netObj := b.conn.Object(iwdBusName, networkPath)
|
||||
go func() {
|
||||
call := netObj.Call(iwdNetworkInterface+".Connect", 0)
|
||||
if call.Err != nil {
|
||||
var code string
|
||||
if dbusErr, ok := call.Err.(dbus.Error); ok {
|
||||
code = b.mapIwdDBusError(dbusErr.Name)
|
||||
} else if dbusErrPtr, ok := call.Err.(*dbus.Error); ok {
|
||||
code = b.mapIwdDBusError(dbusErrPtr.Name)
|
||||
} else {
|
||||
code = errdefs.ErrConnectionFailed
|
||||
}
|
||||
|
||||
att.mu.Lock()
|
||||
if att.sawPromptRetry {
|
||||
code = errdefs.ErrBadCredentials
|
||||
}
|
||||
att.mu.Unlock()
|
||||
|
||||
b.finalizeAttempt(att, code)
|
||||
return
|
||||
}
|
||||
|
||||
b.startAttemptWatchdog(att)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) findNetworkPath(ssid string) (dbus.ObjectPath, error) {
|
||||
obj := b.conn.Object(iwdBusName, iwdObjectPath)
|
||||
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for path, interfaces := range objects {
|
||||
if netProps, ok := interfaces[iwdNetworkInterface]; ok {
|
||||
if nameVar, ok := netProps["Name"]; ok {
|
||||
if name, ok := nameVar.Value().(string); ok && name == ssid {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("network not found")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) DisconnectWiFi() error {
|
||||
if b.stationPath == "" {
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
obj := b.conn.Object(iwdBusName, b.stationPath)
|
||||
call := obj.Call(iwdStationInterface+".Disconnect", 0)
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to disconnect: %w", call.Err)
|
||||
}
|
||||
|
||||
b.updateState()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) ForgetWiFiNetwork(ssid string) error {
|
||||
b.stateMutex.RLock()
|
||||
currentSSID := b.state.WiFiSSID
|
||||
isConnected := b.state.WiFiConnected
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
obj := b.conn.Object(iwdBusName, iwdObjectPath)
|
||||
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for path, interfaces := range objects {
|
||||
if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok {
|
||||
if nameVar, ok := knownProps["Name"]; ok {
|
||||
if name, ok := nameVar.Value().(string); ok && name == ssid {
|
||||
knownObj := b.conn.Object(iwdBusName, path)
|
||||
call := knownObj.Call(iwdKnownNetworkInterface+".Forget", 0)
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to forget network: %w", call.Err)
|
||||
}
|
||||
|
||||
if isConnected && currentSSID == ssid {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiConnected = false
|
||||
b.state.WiFiSSID = ""
|
||||
b.state.WiFiSignal = 0
|
||||
b.state.WiFiIP = ""
|
||||
b.state.NetworkStatus = StatusDisconnected
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("network not found")
|
||||
}
|
||||
|
||||
func (b *IWDBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
|
||||
obj := b.conn.Object(iwdBusName, iwdObjectPath)
|
||||
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for path, interfaces := range objects {
|
||||
if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok {
|
||||
if nameVar, ok := knownProps["Name"]; ok {
|
||||
if name, ok := nameVar.Value().(string); ok && name == ssid {
|
||||
knownObj := b.conn.Object(iwdBusName, path)
|
||||
call := knownObj.Call(dbusPropertiesInterface+".Set", 0, iwdKnownNetworkInterface, "AutoConnect", dbus.MakeVariant(autoconnect))
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to set autoconnect: %w", call.Err)
|
||||
}
|
||||
|
||||
b.updateState()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("network not found")
|
||||
}
|
||||
268
core/internal/server/network/backend_networkd.go
Normal file
268
core/internal/server/network/backend_networkd.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
networkdBusName = "org.freedesktop.network1"
|
||||
networkdManagerPath = "/org/freedesktop/network1"
|
||||
networkdManagerIface = "org.freedesktop.network1.Manager"
|
||||
networkdLinkIface = "org.freedesktop.network1.Link"
|
||||
)
|
||||
|
||||
type linkInfo struct {
|
||||
ifindex int32
|
||||
name string
|
||||
path dbus.ObjectPath
|
||||
opState string
|
||||
}
|
||||
|
||||
type SystemdNetworkdBackend struct {
|
||||
conn *dbus.Conn
|
||||
managerPath dbus.ObjectPath
|
||||
links map[string]*linkInfo
|
||||
linksMutex sync.RWMutex
|
||||
state *BackendState
|
||||
stateMutex sync.RWMutex
|
||||
onStateChange func()
|
||||
stopChan chan struct{}
|
||||
signals chan *dbus.Signal
|
||||
sigWG sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewSystemdNetworkdBackend() (*SystemdNetworkdBackend, error) {
|
||||
return &SystemdNetworkdBackend{
|
||||
managerPath: networkdManagerPath,
|
||||
links: make(map[string]*linkInfo),
|
||||
state: &BackendState{
|
||||
Backend: "networkd",
|
||||
WiFiNetworks: []WiFiNetwork{},
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) Initialize() error {
|
||||
c, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect bus: %w", err)
|
||||
}
|
||||
b.conn = c
|
||||
|
||||
if err := b.enumerateLinks(); err != nil {
|
||||
c.Close()
|
||||
return fmt.Errorf("enumerate links: %w", err)
|
||||
}
|
||||
|
||||
if err := b.updateState(); err != nil {
|
||||
c.Close()
|
||||
return fmt.Errorf("update initial state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) Close() {
|
||||
close(b.stopChan)
|
||||
b.StopMonitoring()
|
||||
|
||||
if b.conn != nil {
|
||||
b.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) enumerateLinks() error {
|
||||
obj := b.conn.Object(networkdBusName, b.managerPath)
|
||||
|
||||
var links []struct {
|
||||
Ifindex int32
|
||||
Name string
|
||||
Path dbus.ObjectPath
|
||||
}
|
||||
err := obj.Call(networkdManagerIface+".ListLinks", 0).Store(&links)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ListLinks: %w", err)
|
||||
}
|
||||
|
||||
b.linksMutex.Lock()
|
||||
defer b.linksMutex.Unlock()
|
||||
|
||||
for _, l := range links {
|
||||
b.links[l.Name] = &linkInfo{
|
||||
ifindex: l.Ifindex,
|
||||
name: l.Name,
|
||||
path: l.Path,
|
||||
}
|
||||
log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s)", l.Name, l.Ifindex, l.Path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) updateState() error {
|
||||
b.linksMutex.RLock()
|
||||
defer b.linksMutex.RUnlock()
|
||||
|
||||
var wiredIface *linkInfo
|
||||
var wifiIface *linkInfo
|
||||
|
||||
for name, link := range b.links {
|
||||
if b.isVirtualInterface(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
linkObj := b.conn.Object(networkdBusName, link.path)
|
||||
opStateVar, err := linkObj.GetProperty(networkdLinkIface + ".OperationalState")
|
||||
if err == nil {
|
||||
if opState, ok := opStateVar.Value().(string); ok {
|
||||
link.opState = opState
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
||||
if wifiIface == nil || link.opState == "routable" || link.opState == "carrier" {
|
||||
wifiIface = link
|
||||
}
|
||||
} else if !b.isVirtualInterface(name) {
|
||||
if wiredIface == nil || link.opState == "routable" || link.opState == "carrier" {
|
||||
wiredIface = link
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wiredConns []WiredConnection
|
||||
for name, link := range b.links {
|
||||
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
||||
continue
|
||||
}
|
||||
|
||||
active := link.opState == "routable" || link.opState == "carrier"
|
||||
wiredConns = append(wiredConns, WiredConnection{
|
||||
Path: link.path,
|
||||
ID: name,
|
||||
UUID: "wired:" + name,
|
||||
Type: "ethernet",
|
||||
IsActive: active,
|
||||
})
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
defer b.stateMutex.Unlock()
|
||||
|
||||
b.state.NetworkStatus = StatusDisconnected
|
||||
b.state.EthernetConnected = false
|
||||
b.state.EthernetIP = ""
|
||||
b.state.WiFiConnected = false
|
||||
b.state.WiFiIP = ""
|
||||
b.state.WiredConnections = wiredConns
|
||||
|
||||
if wiredIface != nil {
|
||||
b.state.EthernetDevice = wiredIface.name
|
||||
log.Debugf("networkd: wired interface %s opState=%s", wiredIface.name, wiredIface.opState)
|
||||
if wiredIface.opState == "routable" || wiredIface.opState == "carrier" {
|
||||
b.state.EthernetConnected = true
|
||||
b.state.NetworkStatus = StatusEthernet
|
||||
|
||||
if addrs := b.getAddresses(wiredIface.name); len(addrs) > 0 {
|
||||
b.state.EthernetIP = addrs[0]
|
||||
log.Debugf("networkd: ethernet IP %s on %s", addrs[0], wiredIface.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wifiIface != nil {
|
||||
b.state.WiFiDevice = wifiIface.name
|
||||
log.Debugf("networkd: wifi interface %s opState=%s", wifiIface.name, wifiIface.opState)
|
||||
if wifiIface.opState == "routable" || wifiIface.opState == "carrier" {
|
||||
b.state.WiFiConnected = true
|
||||
|
||||
if addrs := b.getAddresses(wifiIface.name); len(addrs) > 0 {
|
||||
b.state.WiFiIP = addrs[0]
|
||||
log.Debugf("networkd: wifi IP %s on %s", addrs[0], wifiIface.name)
|
||||
if b.state.NetworkStatus == StatusDisconnected {
|
||||
b.state.NetworkStatus = StatusWiFi
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) isVirtualInterface(name string) bool {
|
||||
virtualPrefixes := []string{
|
||||
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap",
|
||||
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
|
||||
}
|
||||
for _, prefix := range virtualPrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) getAddresses(ifname string) []string {
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
|
||||
result = append(result, ipv4.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetCurrentState() (*BackendState, error) {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
s := *b.state
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetPromptBroker() PromptBroker {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) SetPromptBroker(broker PromptBroker) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error {
|
||||
return fmt.Errorf("credentials not needed by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) CancelCredentials(token string) error {
|
||||
return fmt.Errorf("credentials not needed by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) EnsureDhcpUp(ifname string) error {
|
||||
b.linksMutex.RLock()
|
||||
link, exists := b.links[ifname]
|
||||
b.linksMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("interface %s not found", ifname)
|
||||
}
|
||||
|
||||
linkObj := b.conn.Object(networkdBusName, link.path)
|
||||
return linkObj.Call(networkdLinkIface+".Reconfigure", 0).Err
|
||||
}
|
||||
110
core/internal/server/network/backend_networkd_ethernet.go
Normal file
110
core/internal/server/network/backend_networkd_ethernet.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
b.linksMutex.RLock()
|
||||
defer b.linksMutex.RUnlock()
|
||||
|
||||
var conns []WiredConnection
|
||||
for name, link := range b.links {
|
||||
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
||||
continue
|
||||
}
|
||||
|
||||
active := link.opState == "routable" || link.opState == "carrier"
|
||||
conns = append(conns, WiredConnection{
|
||||
Path: link.path,
|
||||
ID: name,
|
||||
UUID: "wired:" + name,
|
||||
Type: "ethernet",
|
||||
IsActive: active,
|
||||
})
|
||||
}
|
||||
|
||||
return conns, nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetWiredNetworkDetails(id string) (*WiredNetworkInfoResponse, error) {
|
||||
ifname := strings.TrimPrefix(id, "wired:")
|
||||
|
||||
b.linksMutex.RLock()
|
||||
_, exists := b.links[ifname]
|
||||
b.linksMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("interface %s not found", ifname)
|
||||
}
|
||||
|
||||
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 {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
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
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ConnectEthernet() error {
|
||||
b.linksMutex.RLock()
|
||||
var primaryWired *linkInfo
|
||||
for name, l := range b.links {
|
||||
if strings.HasPrefix(name, "lo") || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
||||
continue
|
||||
}
|
||||
primaryWired = l
|
||||
break
|
||||
}
|
||||
b.linksMutex.RUnlock()
|
||||
|
||||
if primaryWired == nil {
|
||||
return fmt.Errorf("no wired interface found")
|
||||
}
|
||||
|
||||
linkObj := b.conn.Object(networkdBusName, primaryWired.path)
|
||||
return linkObj.Call(networkdLinkIface+".Reconfigure", 0).Err
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) DisconnectEthernet() error {
|
||||
return fmt.Errorf("not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ActivateWiredConnection(id string) error {
|
||||
ifname := strings.TrimPrefix(id, "wired:")
|
||||
|
||||
b.linksMutex.RLock()
|
||||
link, exists := b.links[ifname]
|
||||
b.linksMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("interface %s not found", ifname)
|
||||
}
|
||||
|
||||
linkObj := b.conn.Object(networkdBusName, link.path)
|
||||
return linkObj.Call(networkdLinkIface+".Reconfigure", 0).Err
|
||||
}
|
||||
68
core/internal/server/network/backend_networkd_signals.go
Normal file
68
core/internal/server/network/backend_networkd_signals.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func (b *SystemdNetworkdBackend) StartMonitoring(onStateChange func()) error {
|
||||
b.onStateChange = onStateChange
|
||||
|
||||
b.signals = make(chan *dbus.Signal, 64)
|
||||
b.conn.Signal(b.signals)
|
||||
|
||||
matchRules := []string{
|
||||
"type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path_namespace='/org/freedesktop/network1'",
|
||||
"type='signal',interface='org.freedesktop.network1.Manager'",
|
||||
}
|
||||
|
||||
for _, rule := range matchRules {
|
||||
if err := b.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, rule).Err; err != nil {
|
||||
return fmt.Errorf("add match %q: %w", rule, err)
|
||||
}
|
||||
}
|
||||
|
||||
b.sigWG.Add(1)
|
||||
go b.signalLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) StopMonitoring() {
|
||||
b.sigWG.Wait()
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) signalLoop() {
|
||||
defer b.sigWG.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
case sig := <-b.signals:
|
||||
if sig == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if sig.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
|
||||
if len(sig.Body) < 2 {
|
||||
continue
|
||||
}
|
||||
iface, ok := sig.Body[0].(string)
|
||||
if !ok || iface != networkdLinkIface {
|
||||
continue
|
||||
}
|
||||
|
||||
b.enumerateLinks()
|
||||
if err := b.updateState(); err != nil {
|
||||
log.Warnf("networkd state update failed: %v", err)
|
||||
}
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
core/internal/server/network/backend_networkd_test.go
Normal file
125
core/internal/server/network/backend_networkd_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSystemdNetworkdBackend_New(t *testing.T) {
|
||||
backend, err := NewSystemdNetworkdBackend()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, backend)
|
||||
assert.Equal(t, "networkd", backend.state.Backend)
|
||||
assert.NotNil(t, backend.links)
|
||||
assert.NotNil(t, backend.stopChan)
|
||||
}
|
||||
|
||||
func TestSystemdNetworkdBackend_GetCurrentState(t *testing.T) {
|
||||
backend, _ := NewSystemdNetworkdBackend()
|
||||
backend.state.NetworkStatus = StatusEthernet
|
||||
backend.state.EthernetConnected = true
|
||||
backend.state.EthernetIP = "192.168.1.100"
|
||||
|
||||
state, err := backend.GetCurrentState()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, state)
|
||||
assert.Equal(t, StatusEthernet, state.NetworkStatus)
|
||||
assert.True(t, state.EthernetConnected)
|
||||
assert.Equal(t, "192.168.1.100", state.EthernetIP)
|
||||
}
|
||||
|
||||
func TestSystemdNetworkdBackend_WiFiNotSupported(t *testing.T) {
|
||||
backend, _ := NewSystemdNetworkdBackend()
|
||||
|
||||
err := backend.ScanWiFi()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
|
||||
req := ConnectionRequest{SSID: "test"}
|
||||
err = backend.ConnectWiFi(req)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
|
||||
err = backend.DisconnectWiFi()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
|
||||
err = backend.ForgetWiFiNetwork("test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
|
||||
_, err = backend.GetWiFiNetworkDetails("test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
}
|
||||
|
||||
func TestSystemdNetworkdBackend_VPNNotSupported(t *testing.T) {
|
||||
backend, _ := NewSystemdNetworkdBackend()
|
||||
|
||||
profiles, err := backend.ListVPNProfiles()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, profiles)
|
||||
|
||||
active, err := backend.ListActiveVPN()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, active)
|
||||
|
||||
err = backend.ConnectVPN("test", false)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
|
||||
err = backend.DisconnectVPN("test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
|
||||
err = backend.DisconnectAllVPN()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
|
||||
err = backend.ClearVPNCredentials("test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
}
|
||||
|
||||
func TestSystemdNetworkdBackend_PromptBroker(t *testing.T) {
|
||||
backend, _ := NewSystemdNetworkdBackend()
|
||||
|
||||
broker := backend.GetPromptBroker()
|
||||
assert.Nil(t, broker)
|
||||
|
||||
err := backend.SetPromptBroker(nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = backend.SubmitCredentials("token", nil, false)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not needed")
|
||||
|
||||
err = backend.CancelCredentials("token")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not needed")
|
||||
}
|
||||
|
||||
func TestSystemdNetworkdBackend_GetWiFiEnabled(t *testing.T) {
|
||||
backend, _ := NewSystemdNetworkdBackend()
|
||||
|
||||
enabled, err := backend.GetWiFiEnabled()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, enabled)
|
||||
}
|
||||
|
||||
func TestSystemdNetworkdBackend_SetWiFiEnabled(t *testing.T) {
|
||||
backend, _ := NewSystemdNetworkdBackend()
|
||||
|
||||
err := backend.SetWiFiEnabled(false)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
}
|
||||
|
||||
func TestSystemdNetworkdBackend_DisconnectEthernet(t *testing.T) {
|
||||
backend, _ := NewSystemdNetworkdBackend()
|
||||
|
||||
err := backend.DisconnectEthernet()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package network
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetWiFiEnabled() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) SetWiFiEnabled(enabled bool) error {
|
||||
return fmt.Errorf("WiFi control not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ScanWiFi() error {
|
||||
return fmt.Errorf("WiFi scan not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) {
|
||||
return nil, fmt.Errorf("WiFi details not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
return fmt.Errorf("WiFi connect not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) DisconnectWiFi() error {
|
||||
return fmt.Errorf("WiFi disconnect not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ForgetWiFiNetwork(ssid string) error {
|
||||
return fmt.Errorf("WiFi forget not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
||||
return []VPNProfile{}, nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ListActiveVPN() ([]VPNActive, error) {
|
||||
return []VPNActive{}, nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||
return fmt.Errorf("VPN not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) DisconnectVPN(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) DisconnectAllVPN() error {
|
||||
return fmt.Errorf("VPN not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ClearVPNCredentials(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
|
||||
return fmt.Errorf("WiFi autoconnect not supported by networkd backend")
|
||||
}
|
||||
307
core/internal/server/network/backend_networkmanager.go
Normal file
307
core/internal/server/network/backend_networkmanager.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusNMPath = "/org/freedesktop/NetworkManager"
|
||||
dbusNMInterface = "org.freedesktop.NetworkManager"
|
||||
dbusNMDeviceInterface = "org.freedesktop.NetworkManager.Device"
|
||||
dbusNMWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless"
|
||||
dbusNMAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint"
|
||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||
|
||||
NmDeviceStateReasonWrongPassword = 8
|
||||
NmDeviceStateReasonSupplicantTimeout = 24
|
||||
NmDeviceStateReasonSupplicantFailed = 25
|
||||
NmDeviceStateReasonSecretsRequired = 7
|
||||
NmDeviceStateReasonNoSecrets = 6
|
||||
NmDeviceStateReasonNoSsid = 10
|
||||
NmDeviceStateReasonDhcpClientFailed = 14
|
||||
NmDeviceStateReasonIpConfigUnavailable = 18
|
||||
NmDeviceStateReasonSupplicantDisconnect = 23
|
||||
NmDeviceStateReasonCarrier = 40
|
||||
NmDeviceStateReasonNewActivation = 60
|
||||
)
|
||||
|
||||
type NetworkManagerBackend struct {
|
||||
nmConn interface{}
|
||||
ethernetDevice interface{}
|
||||
wifiDevice interface{}
|
||||
settings interface{}
|
||||
wifiDev interface{}
|
||||
|
||||
dbusConn *dbus.Conn
|
||||
signals chan *dbus.Signal
|
||||
sigWG sync.WaitGroup
|
||||
stopChan chan struct{}
|
||||
|
||||
secretAgent *SecretAgent
|
||||
promptBroker PromptBroker
|
||||
|
||||
state *BackendState
|
||||
stateMutex sync.RWMutex
|
||||
|
||||
lastFailedSSID string
|
||||
lastFailedTime int64
|
||||
failedMutex sync.RWMutex
|
||||
|
||||
onStateChange func()
|
||||
}
|
||||
|
||||
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
|
||||
var nm gonetworkmanager.NetworkManager
|
||||
var err error
|
||||
|
||||
if len(nmConn) > 0 && nmConn[0] != nil {
|
||||
// Use injected connection (for testing)
|
||||
nm = nmConn[0]
|
||||
} else {
|
||||
// Create real connection
|
||||
nm, err = gonetworkmanager.NewNetworkManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to NetworkManager: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
backend := &NetworkManagerBackend{
|
||||
nmConn: nm,
|
||||
stopChan: make(chan struct{}),
|
||||
state: &BackendState{
|
||||
Backend: "networkmanager",
|
||||
},
|
||||
}
|
||||
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) Initialize() error {
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
|
||||
if s, err := gonetworkmanager.NewSettings(); err == nil {
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
devices, err := nm.GetDevices()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get devices: %w", err)
|
||||
}
|
||||
|
||||
for _, dev := range devices {
|
||||
devType, err := dev.GetPropertyDeviceType()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch devType {
|
||||
case gonetworkmanager.NmDeviceTypeEthernet:
|
||||
if managed, _ := dev.GetPropertyManaged(); !managed {
|
||||
continue
|
||||
}
|
||||
b.ethernetDevice = dev
|
||||
if err := b.updateEthernetState(); err != nil {
|
||||
continue
|
||||
}
|
||||
_, err := b.listEthernetConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get wired configurations: %w", err)
|
||||
}
|
||||
|
||||
case gonetworkmanager.NmDeviceTypeWifi:
|
||||
b.wifiDevice = dev
|
||||
if w, err := gonetworkmanager.NewDeviceWireless(dev.GetPath()); err == nil {
|
||||
b.wifiDev = w
|
||||
}
|
||||
wifiEnabled, err := nm.GetPropertyWirelessEnabled()
|
||||
if err == nil {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiEnabled = wifiEnabled
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
if err := b.updateWiFiState(); err != nil {
|
||||
continue
|
||||
}
|
||||
if wifiEnabled {
|
||||
if _, err := b.updateWiFiNetworks(); err != nil {
|
||||
log.Warnf("Failed to get initial networks: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.updatePrimaryConnection(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := b.ListVPNProfiles(); err != nil {
|
||||
log.Warnf("Failed to get initial VPN profiles: %v", err)
|
||||
}
|
||||
|
||||
if _, err := b.ListActiveVPN(); err != nil {
|
||||
log.Warnf("Failed to get initial active VPNs: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) Close() {
|
||||
close(b.stopChan)
|
||||
b.StopMonitoring()
|
||||
|
||||
if b.secretAgent != nil {
|
||||
b.secretAgent.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) GetCurrentState() (*BackendState, error) {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
|
||||
state := *b.state
|
||||
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
|
||||
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
|
||||
state.VPNProfiles = append([]VPNProfile(nil), b.state.VPNProfiles...)
|
||||
state.VPNActive = append([]VPNActive(nil), b.state.VPNActive...)
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) StartMonitoring(onStateChange func()) error {
|
||||
b.onStateChange = onStateChange
|
||||
|
||||
if err := b.startSecretAgent(); err != nil {
|
||||
return fmt.Errorf("failed to start secret agent: %w", err)
|
||||
}
|
||||
|
||||
if err := b.startSignalPump(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) StopMonitoring() {
|
||||
b.stopSignalPump()
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) GetPromptBroker() PromptBroker {
|
||||
return b.promptBroker
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) SetPromptBroker(broker PromptBroker) error {
|
||||
if broker == nil {
|
||||
return fmt.Errorf("broker cannot be nil")
|
||||
}
|
||||
|
||||
hadAgent := b.secretAgent != nil
|
||||
|
||||
b.promptBroker = broker
|
||||
|
||||
if b.secretAgent != nil {
|
||||
b.secretAgent.Close()
|
||||
b.secretAgent = nil
|
||||
}
|
||||
|
||||
if hadAgent {
|
||||
return b.startSecretAgent()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) 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 *NetworkManagerBackend) 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 *NetworkManagerBackend) ensureWiFiDevice() error {
|
||||
if b.wifiDev != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.wifiDevice == nil {
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
dev := b.wifiDevice.(gonetworkmanager.Device)
|
||||
wifiDev, err := gonetworkmanager.NewDeviceWireless(dev.GetPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get wireless device: %w", err)
|
||||
}
|
||||
b.wifiDev = wifiDev
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) startSecretAgent() error {
|
||||
if b.promptBroker == nil {
|
||||
return fmt.Errorf("prompt broker not set")
|
||||
}
|
||||
|
||||
agent, err := NewSecretAgent(b.promptBroker, nil, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.secretAgent = agent
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) getActiveConnections() (map[string]bool, error) {
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
|
||||
activeUUIDs := make(map[string]bool)
|
||||
|
||||
activeConns, err := nm.GetPropertyActiveConnections()
|
||||
if err != nil {
|
||||
return activeUUIDs, fmt.Errorf("failed to get active connections: %w", err)
|
||||
}
|
||||
|
||||
for _, activeConn := range activeConns {
|
||||
connType, err := activeConn.GetPropertyType()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connType != "802-3-ethernet" {
|
||||
continue
|
||||
}
|
||||
|
||||
state, err := activeConn.GetPropertyState()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if state < 1 || state > 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
uuid, err := activeConn.GetPropertyUUID()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
activeUUIDs[uuid] = true
|
||||
}
|
||||
return activeUUIDs, nil
|
||||
}
|
||||
317
core/internal/server/network/backend_networkmanager_ethernet.go
Normal file
317
core/internal/server/network/backend_networkmanager_ethernet.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
)
|
||||
|
||||
func (b *NetworkManagerBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
return b.listEthernetConnections()
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error) {
|
||||
if b.ethernetDevice == nil {
|
||||
return nil, fmt.Errorf("no ethernet device available")
|
||||
}
|
||||
|
||||
dev := b.ethernetDevice.(gonetworkmanager.Device)
|
||||
|
||||
iface, _ := dev.GetPropertyInterface()
|
||||
driver, _ := dev.GetPropertyDriver()
|
||||
|
||||
hwAddr := "Not available"
|
||||
var speed uint32 = 0
|
||||
wiredDevice, err := gonetworkmanager.NewDeviceWired(dev.GetPath())
|
||||
if err == nil {
|
||||
hwAddr, _ = wiredDevice.GetPropertyHwAddress()
|
||||
speed, _ = wiredDevice.GetPropertySpeed()
|
||||
}
|
||||
var ipv4Config WiredIPConfig
|
||||
var ipv6Config WiredIPConfig
|
||||
|
||||
activeConn, err := dev.GetPropertyActiveConnection()
|
||||
if err == nil && activeConn != nil {
|
||||
ip4Config, err := activeConn.GetPropertyIP4Config()
|
||||
if err == nil && ip4Config != nil {
|
||||
var ips []string
|
||||
addresses, err := ip4Config.GetPropertyAddressData()
|
||||
if err == nil && len(addresses) > 0 {
|
||||
for _, addr := range addresses {
|
||||
ips = append(ips, fmt.Sprintf("%s/%s", addr.Address, strconv.Itoa(int(addr.Prefix))))
|
||||
}
|
||||
}
|
||||
|
||||
gateway, _ := ip4Config.GetPropertyGateway()
|
||||
dnsAddrs := ""
|
||||
dns, err := ip4Config.GetPropertyNameserverData()
|
||||
if err == nil && len(dns) > 0 {
|
||||
for _, d := range dns {
|
||||
if len(dnsAddrs) > 0 {
|
||||
dnsAddrs = strings.Join([]string{dnsAddrs, d.Address}, "; ")
|
||||
} else {
|
||||
dnsAddrs = d.Address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ipv4Config = WiredIPConfig{
|
||||
IPs: ips,
|
||||
Gateway: gateway,
|
||||
DNS: dnsAddrs,
|
||||
}
|
||||
}
|
||||
|
||||
ip6Config, err := activeConn.GetPropertyIP6Config()
|
||||
if err == nil && ip6Config != nil {
|
||||
var ips []string
|
||||
addresses, err := ip6Config.GetPropertyAddressData()
|
||||
if err == nil && len(addresses) > 0 {
|
||||
for _, addr := range addresses {
|
||||
ips = append(ips, fmt.Sprintf("%s/%s", addr.Address, strconv.Itoa(int(addr.Prefix))))
|
||||
}
|
||||
}
|
||||
|
||||
gateway, _ := ip6Config.GetPropertyGateway()
|
||||
dnsAddrs := ""
|
||||
dns, err := ip6Config.GetPropertyNameservers()
|
||||
if err == nil && len(dns) > 0 {
|
||||
for _, d := range dns {
|
||||
if len(d) == 16 {
|
||||
ip := net.IP(d)
|
||||
if len(dnsAddrs) > 0 {
|
||||
dnsAddrs = strings.Join([]string{dnsAddrs, ip.String()}, "; ")
|
||||
} else {
|
||||
dnsAddrs = ip.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ipv6Config = WiredIPConfig{
|
||||
IPs: ips,
|
||||
Gateway: gateway,
|
||||
DNS: dnsAddrs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &WiredNetworkInfoResponse{
|
||||
UUID: uuid,
|
||||
IFace: iface,
|
||||
Driver: driver,
|
||||
HwAddr: hwAddr,
|
||||
Speed: strconv.Itoa(int(speed)),
|
||||
IPv4: ipv4Config,
|
||||
IPv6: ipv6Config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ConnectEthernet() error {
|
||||
if b.ethernetDevice == nil {
|
||||
return fmt.Errorf("no ethernet device available")
|
||||
}
|
||||
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
dev := b.ethernetDevice.(gonetworkmanager.Device)
|
||||
|
||||
settingsMgr, err := gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
for _, conn := range connections {
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connMeta, ok := connSettings["connection"]; ok {
|
||||
if connType, ok := connMeta["type"].(string); ok && connType == "802-3-ethernet" {
|
||||
_, err := nm.ActivateConnection(conn, dev, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to activate ethernet: %w", err)
|
||||
}
|
||||
|
||||
b.updateEthernetState()
|
||||
b.listEthernetConnections()
|
||||
b.updatePrimaryConnection()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings := make(map[string]map[string]interface{})
|
||||
settings["connection"] = map[string]interface{}{
|
||||
"id": "Wired connection",
|
||||
"type": "802-3-ethernet",
|
||||
}
|
||||
|
||||
_, err = nm.AddAndActivateConnection(settings, dev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create and activate ethernet: %w", err)
|
||||
}
|
||||
|
||||
b.updateEthernetState()
|
||||
b.listEthernetConnections()
|
||||
b.updatePrimaryConnection()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) DisconnectEthernet() error {
|
||||
if b.ethernetDevice == nil {
|
||||
return fmt.Errorf("no ethernet device available")
|
||||
}
|
||||
|
||||
dev := b.ethernetDevice.(gonetworkmanager.Device)
|
||||
|
||||
err := dev.Disconnect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to disconnect: %w", err)
|
||||
}
|
||||
|
||||
b.updateEthernetState()
|
||||
b.listEthernetConnections()
|
||||
b.updatePrimaryConnection()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ActivateWiredConnection(uuid string) error {
|
||||
if b.ethernetDevice == nil {
|
||||
return fmt.Errorf("no ethernet device available")
|
||||
}
|
||||
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
dev := b.ethernetDevice.(gonetworkmanager.Device)
|
||||
|
||||
settingsMgr, err := gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
var targetConnection gonetworkmanager.Connection
|
||||
for _, conn := range connections {
|
||||
settings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connectionSettings, ok := settings["connection"]; ok {
|
||||
if connUUID, ok := connectionSettings["uuid"].(string); ok && connUUID == uuid {
|
||||
targetConnection = conn
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if targetConnection == nil {
|
||||
return fmt.Errorf("connection with UUID %s not found", uuid)
|
||||
}
|
||||
|
||||
_, err = nm.ActivateConnection(targetConnection, dev, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error activation connection: %w", err)
|
||||
}
|
||||
|
||||
b.updateEthernetState()
|
||||
b.listEthernetConnections()
|
||||
b.updatePrimaryConnection()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) listEthernetConnections() ([]WiredConnection, error) {
|
||||
if b.ethernetDevice == nil {
|
||||
return nil, fmt.Errorf("no ethernet device available")
|
||||
}
|
||||
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
s, err := gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settingsMgr := s.(gonetworkmanager.Settings)
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
wiredConfigs := make([]WiredConnection, 0)
|
||||
activeUUIDs, err := b.getActiveConnections()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active wired connections: %w", err)
|
||||
}
|
||||
|
||||
currentUuid := ""
|
||||
for _, connection := range connections {
|
||||
path := connection.GetPath()
|
||||
settings, err := connection.GetSettings()
|
||||
if err != nil {
|
||||
log.Errorf("unable to get settings for %s: %v", path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
connectionSettings := settings["connection"]
|
||||
connType, _ := connectionSettings["type"].(string)
|
||||
connID, _ := connectionSettings["id"].(string)
|
||||
connUUID, _ := connectionSettings["uuid"].(string)
|
||||
|
||||
if connType == "802-3-ethernet" {
|
||||
wiredConfigs = append(wiredConfigs, WiredConnection{
|
||||
Path: path,
|
||||
ID: connID,
|
||||
UUID: connUUID,
|
||||
Type: connType,
|
||||
IsActive: activeUUIDs[connUUID],
|
||||
})
|
||||
if activeUUIDs[connUUID] {
|
||||
currentUuid = connUUID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.EthernetConnectionUuid = currentUuid
|
||||
b.state.WiredConnections = wiredConfigs
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
return wiredConfigs, nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNetworkManagerBackend_GetWiredConnections_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.ethernetDevice = nil
|
||||
_, err = backend.GetWiredConnections()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_GetWiredNetworkDetails_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.ethernetDevice = nil
|
||||
_, err = backend.GetWiredNetworkDetails("test-uuid")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ConnectEthernet_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.ethernetDevice = nil
|
||||
err = backend.ConnectEthernet()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_DisconnectEthernet_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.ethernetDevice = nil
|
||||
err = backend.DisconnectEthernet()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ActivateWiredConnection_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.ethernetDevice = nil
|
||||
err = backend.ActivateWiredConnection("test-uuid")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ActivateWiredConnection_NotFound(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
if backend.ethernetDevice == nil {
|
||||
t.Skip("No ethernet device available")
|
||||
}
|
||||
|
||||
err = backend.ActivateWiredConnection("non-existent-uuid-12345")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ListEthernetConnections_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.ethernetDevice = nil
|
||||
_, err = backend.listEthernetConnections()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
||||
}
|
||||
321
core/internal/server/network/backend_networkmanager_signals.go
Normal file
321
core/internal/server/network/backend_networkmanager_signals.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func (b *NetworkManagerBackend) startSignalPump() error {
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.dbusConn = conn
|
||||
|
||||
signals := make(chan *dbus.Signal, 256)
|
||||
b.signals = signals
|
||||
conn.Signal(signals)
|
||||
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
); err != nil {
|
||||
conn.RemoveSignal(signals)
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
|
||||
dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
|
||||
dbus.WithMatchMember("NewConnection"),
|
||||
); err != nil {
|
||||
conn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
conn.RemoveSignal(signals)
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
|
||||
dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
|
||||
dbus.WithMatchMember("ConnectionRemoved"),
|
||||
); err != nil {
|
||||
conn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
conn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
|
||||
dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
|
||||
dbus.WithMatchMember("NewConnection"),
|
||||
)
|
||||
conn.RemoveSignal(signals)
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if b.wifiDevice != nil {
|
||||
dev := b.wifiDevice.(gonetworkmanager.Device)
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
); err != nil {
|
||||
conn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
conn.RemoveSignal(signals)
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if b.ethernetDevice != nil {
|
||||
dev := b.ethernetDevice.(gonetworkmanager.Device)
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
); err != nil {
|
||||
conn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
if b.wifiDevice != nil {
|
||||
dev := b.wifiDevice.(gonetworkmanager.Device)
|
||||
conn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
}
|
||||
conn.RemoveSignal(signals)
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b.sigWG.Add(1)
|
||||
go func() {
|
||||
defer b.sigWG.Done()
|
||||
for {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
case sig, ok := <-signals:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if sig == nil {
|
||||
continue
|
||||
}
|
||||
b.handleDBusSignal(sig)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) stopSignalPump() {
|
||||
if b.dbusConn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.dbusConn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
|
||||
if b.wifiDevice != nil {
|
||||
dev := b.wifiDevice.(gonetworkmanager.Device)
|
||||
b.dbusConn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
}
|
||||
|
||||
if b.ethernetDevice != nil {
|
||||
dev := b.ethernetDevice.(gonetworkmanager.Device)
|
||||
b.dbusConn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())),
|
||||
dbus.WithMatchInterface(dbusPropsInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
}
|
||||
|
||||
if b.signals != nil {
|
||||
b.dbusConn.RemoveSignal(b.signals)
|
||||
close(b.signals)
|
||||
}
|
||||
|
||||
b.sigWG.Wait()
|
||||
|
||||
b.dbusConn.Close()
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) {
|
||||
if sig.Name == "org.freedesktop.NetworkManager.Settings.NewConnection" ||
|
||||
sig.Name == "org.freedesktop.NetworkManager.Settings.ConnectionRemoved" {
|
||||
b.ListVPNProfiles()
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(sig.Body) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
iface, ok := sig.Body[0].(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
changes, ok := sig.Body[1].(map[string]dbus.Variant)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch iface {
|
||||
case dbusNMInterface:
|
||||
b.handleNetworkManagerChange(changes)
|
||||
|
||||
case dbusNMDeviceInterface:
|
||||
b.handleDeviceChange(changes)
|
||||
|
||||
case dbusNMWirelessInterface:
|
||||
b.handleWiFiChange(changes)
|
||||
|
||||
case dbusNMAccessPointInterface:
|
||||
b.handleAccessPointChange(changes)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleNetworkManagerChange(changes map[string]dbus.Variant) {
|
||||
var needsUpdate bool
|
||||
|
||||
for key := range changes {
|
||||
switch key {
|
||||
case "PrimaryConnection", "State", "ActiveConnections":
|
||||
needsUpdate = true
|
||||
case "WirelessEnabled":
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
if enabled, err := nm.GetPropertyWirelessEnabled(); err == nil {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiEnabled = enabled
|
||||
b.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
b.updatePrimaryConnection()
|
||||
if _, exists := changes["State"]; exists {
|
||||
b.updateEthernetState()
|
||||
b.updateWiFiState()
|
||||
}
|
||||
if _, exists := changes["ActiveConnections"]; exists {
|
||||
b.updateVPNConnectionState()
|
||||
b.ListActiveVPN()
|
||||
}
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleDeviceChange(changes map[string]dbus.Variant) {
|
||||
var needsUpdate bool
|
||||
var stateChanged bool
|
||||
|
||||
for key := range changes {
|
||||
switch key {
|
||||
case "State":
|
||||
stateChanged = true
|
||||
needsUpdate = true
|
||||
case "Ip4Config":
|
||||
needsUpdate = true
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
b.updateEthernetState()
|
||||
b.updateWiFiState()
|
||||
if stateChanged {
|
||||
b.updatePrimaryConnection()
|
||||
}
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleWiFiChange(changes map[string]dbus.Variant) {
|
||||
var needsStateUpdate bool
|
||||
var needsNetworkUpdate bool
|
||||
|
||||
for key := range changes {
|
||||
switch key {
|
||||
case "ActiveAccessPoint":
|
||||
needsStateUpdate = true
|
||||
needsNetworkUpdate = true
|
||||
case "AccessPoints":
|
||||
needsNetworkUpdate = true
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if needsStateUpdate {
|
||||
b.updateWiFiState()
|
||||
}
|
||||
if needsNetworkUpdate {
|
||||
b.updateWiFiNetworks()
|
||||
}
|
||||
if needsStateUpdate || needsNetworkUpdate {
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleAccessPointChange(changes map[string]dbus.Variant) {
|
||||
_, hasStrength := changes["Strength"]
|
||||
if !hasStrength {
|
||||
return
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
oldSignal := b.state.WiFiSignal
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
b.updateWiFiState()
|
||||
|
||||
b.stateMutex.RLock()
|
||||
newSignal := b.state.WiFiSignal
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
if signalChangeSignificant(oldSignal, newSignal) {
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNetworkManagerBackend_HandleDBusSignal_NewConnection(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
sig := &dbus.Signal{
|
||||
Name: "org.freedesktop.NetworkManager.Settings.NewConnection",
|
||||
Body: []interface{}{"/org/freedesktop/NetworkManager/Settings/1"},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleDBusSignal(sig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleDBusSignal_ConnectionRemoved(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
sig := &dbus.Signal{
|
||||
Name: "org.freedesktop.NetworkManager.Settings.ConnectionRemoved",
|
||||
Body: []interface{}{"/org/freedesktop/NetworkManager/Settings/1"},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleDBusSignal(sig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleDBusSignal_InvalidBody(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
sig := &dbus.Signal{
|
||||
Name: "org.freedesktop.DBus.Properties.PropertiesChanged",
|
||||
Body: []interface{}{"only-one-element"},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleDBusSignal(sig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleDBusSignal_InvalidInterface(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
sig := &dbus.Signal{
|
||||
Name: "org.freedesktop.DBus.Properties.PropertiesChanged",
|
||||
Body: []interface{}{123, map[string]dbus.Variant{}},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleDBusSignal(sig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleDBusSignal_InvalidChanges(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
sig := &dbus.Signal{
|
||||
Name: "org.freedesktop.DBus.Properties.PropertiesChanged",
|
||||
Body: []interface{}{dbusNMInterface, "not-a-map"},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleDBusSignal(sig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleNetworkManagerChange(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"PrimaryConnection": dbus.MakeVariant("/"),
|
||||
"State": dbus.MakeVariant(uint32(70)),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleNetworkManagerChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleNetworkManagerChange_WirelessEnabled(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"WirelessEnabled": dbus.MakeVariant(true),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleNetworkManagerChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleNetworkManagerChange_ActiveConnections(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"ActiveConnections": dbus.MakeVariant([]interface{}{}),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleNetworkManagerChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleDeviceChange(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"State": dbus.MakeVariant(uint32(100)),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleDeviceChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleDeviceChange_Ip4Config(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"Ip4Config": dbus.MakeVariant("/"),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleDeviceChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleWiFiChange_ActiveAccessPoint(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"ActiveAccessPoint": dbus.MakeVariant("/"),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleWiFiChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleWiFiChange_AccessPoints(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"AccessPoints": dbus.MakeVariant([]interface{}{}),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleWiFiChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleAccessPointChange_NoStrength(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"SomeOtherProperty": dbus.MakeVariant("value"),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleAccessPointChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_HandleAccessPointChange_WithStrength(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.stateMutex.Lock()
|
||||
backend.state.WiFiSignal = 50
|
||||
backend.stateMutex.Unlock()
|
||||
|
||||
changes := map[string]dbus.Variant{
|
||||
"Strength": dbus.MakeVariant(uint8(80)),
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.handleAccessPointChange(changes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_StopSignalPump_NoConnection(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.dbusConn = nil
|
||||
assert.NotPanics(t, func() {
|
||||
backend.stopSignalPump()
|
||||
})
|
||||
}
|
||||
271
core/internal/server/network/backend_networkmanager_state.go
Normal file
271
core/internal/server/network/backend_networkmanager_state.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
)
|
||||
|
||||
func (b *NetworkManagerBackend) updatePrimaryConnection() error {
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
|
||||
activeConns, err := nm.GetPropertyActiveConnections()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasActiveVPN := false
|
||||
for _, activeConn := range activeConns {
|
||||
connType, err := activeConn.GetPropertyType()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if connType == "vpn" || connType == "wireguard" {
|
||||
state, _ := activeConn.GetPropertyState()
|
||||
if state == 2 {
|
||||
hasActiveVPN = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasActiveVPN {
|
||||
b.stateMutex.Lock()
|
||||
b.state.NetworkStatus = StatusVPN
|
||||
b.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
primaryConn, err := nm.GetPropertyPrimaryConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if primaryConn == nil || primaryConn.GetPath() == "/" {
|
||||
b.stateMutex.Lock()
|
||||
b.state.NetworkStatus = StatusDisconnected
|
||||
b.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
connType, err := primaryConn.GetPropertyType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
switch connType {
|
||||
case "802-3-ethernet":
|
||||
b.state.NetworkStatus = StatusEthernet
|
||||
case "802-11-wireless":
|
||||
b.state.NetworkStatus = StatusWiFi
|
||||
case "vpn", "wireguard":
|
||||
b.state.NetworkStatus = StatusVPN
|
||||
default:
|
||||
b.state.NetworkStatus = StatusDisconnected
|
||||
}
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) updateEthernetState() error {
|
||||
if b.ethernetDevice == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dev := b.ethernetDevice.(gonetworkmanager.Device)
|
||||
|
||||
iface, err := dev.GetPropertyInterface()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state, err := dev.GetPropertyState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
connected := state == gonetworkmanager.NmDeviceStateActivated
|
||||
|
||||
var ip string
|
||||
if connected {
|
||||
ip = b.getDeviceIP(dev)
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.EthernetDevice = iface
|
||||
b.state.EthernetConnected = connected
|
||||
b.state.EthernetIP = ip
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) getDeviceStateReason(dev gonetworkmanager.Device) uint32 {
|
||||
path := dev.GetPath()
|
||||
obj := b.dbusConn.Object("org.freedesktop.NetworkManager", path)
|
||||
|
||||
variant, err := obj.GetProperty(dbusNMDeviceInterface + ".StateReason")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
if stateReasonStruct, ok := variant.Value().([]interface{}); ok && len(stateReasonStruct) >= 2 {
|
||||
if reason, ok := stateReasonStruct[1].(uint32); ok {
|
||||
return reason
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) classifyNMStateReason(reason uint32) string {
|
||||
switch reason {
|
||||
case NmDeviceStateReasonWrongPassword,
|
||||
NmDeviceStateReasonSupplicantTimeout,
|
||||
NmDeviceStateReasonSupplicantFailed,
|
||||
NmDeviceStateReasonSecretsRequired:
|
||||
return errdefs.ErrBadCredentials
|
||||
case NmDeviceStateReasonNoSecrets:
|
||||
return errdefs.ErrUserCanceled
|
||||
case NmDeviceStateReasonNoSsid:
|
||||
return errdefs.ErrNoSuchSSID
|
||||
case NmDeviceStateReasonDhcpClientFailed,
|
||||
NmDeviceStateReasonIpConfigUnavailable:
|
||||
return errdefs.ErrDhcpTimeout
|
||||
case NmDeviceStateReasonSupplicantDisconnect,
|
||||
NmDeviceStateReasonCarrier:
|
||||
return errdefs.ErrAssocTimeout
|
||||
default:
|
||||
return errdefs.ErrConnectionFailed
|
||||
}
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) updateWiFiState() error {
|
||||
if b.wifiDevice == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dev := b.wifiDevice.(gonetworkmanager.Device)
|
||||
|
||||
iface, err := dev.GetPropertyInterface()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state, err := dev.GetPropertyState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
connected := state == gonetworkmanager.NmDeviceStateActivated
|
||||
failed := state == gonetworkmanager.NmDeviceStateFailed
|
||||
disconnected := state == gonetworkmanager.NmDeviceStateDisconnected
|
||||
|
||||
var ip, ssid, bssid string
|
||||
var signal uint8
|
||||
|
||||
if connected {
|
||||
if err := b.ensureWiFiDevice(); err == nil && b.wifiDev != nil {
|
||||
w := b.wifiDev.(gonetworkmanager.DeviceWireless)
|
||||
activeAP, err := w.GetPropertyActiveAccessPoint()
|
||||
if err == nil && activeAP != nil && activeAP.GetPath() != "/" {
|
||||
ssid, _ = activeAP.GetPropertySSID()
|
||||
signal, _ = activeAP.GetPropertyStrength()
|
||||
bssid, _ = activeAP.GetPropertyHWAddress()
|
||||
}
|
||||
}
|
||||
|
||||
ip = b.getDeviceIP(dev)
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
wasConnecting := b.state.IsConnecting
|
||||
connectingSSID := b.state.ConnectingSSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
var reasonCode string
|
||||
if wasConnecting && connectingSSID != "" && (failed || (disconnected && !connected)) {
|
||||
reason := b.getDeviceStateReason(dev)
|
||||
|
||||
if reason == NmDeviceStateReasonNewActivation || reason == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d, reason=%d", connectingSSID, state, reason)
|
||||
|
||||
reasonCode = b.classifyNMStateReason(reason)
|
||||
|
||||
if reasonCode == errdefs.ErrConnectionFailed {
|
||||
b.failedMutex.RLock()
|
||||
if b.lastFailedSSID == connectingSSID {
|
||||
elapsed := time.Now().Unix() - b.lastFailedTime
|
||||
if elapsed < 5 {
|
||||
reasonCode = errdefs.ErrBadCredentials
|
||||
}
|
||||
}
|
||||
b.failedMutex.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
defer b.stateMutex.Unlock()
|
||||
|
||||
wasConnecting = b.state.IsConnecting
|
||||
connectingSSID = b.state.ConnectingSSID
|
||||
|
||||
if wasConnecting && connectingSSID != "" {
|
||||
if connected && ssid == connectingSSID {
|
||||
log.Infof("[updateWiFiState] Connection successful: %s", ssid)
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = ""
|
||||
} else if failed || (disconnected && !connected) {
|
||||
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = reasonCode
|
||||
|
||||
// If user cancelled, delete the connection profile that was just created
|
||||
if reasonCode == errdefs.ErrUserCanceled {
|
||||
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", connectingSSID)
|
||||
b.stateMutex.Unlock()
|
||||
if err := b.ForgetWiFiNetwork(connectingSSID); err != nil {
|
||||
log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err)
|
||||
}
|
||||
b.stateMutex.Lock()
|
||||
}
|
||||
|
||||
b.failedMutex.Lock()
|
||||
b.lastFailedSSID = connectingSSID
|
||||
b.lastFailedTime = time.Now().Unix()
|
||||
b.failedMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
b.state.WiFiDevice = iface
|
||||
b.state.WiFiConnected = connected
|
||||
b.state.WiFiIP = ip
|
||||
b.state.WiFiSSID = ssid
|
||||
b.state.WiFiBSSID = bssid
|
||||
b.state.WiFiSignal = signal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) getDeviceIP(dev gonetworkmanager.Device) string {
|
||||
ip4Config, err := dev.GetPropertyIP4Config()
|
||||
if err != nil || ip4Config == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
addresses, err := ip4Config.GetPropertyAddressData()
|
||||
if err != nil || len(addresses) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return addresses[0].Address
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNetworkManagerBackend_UpdatePrimaryConnection(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil)
|
||||
mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil)
|
||||
|
||||
err = backend.updatePrimaryConnection()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_UpdateEthernetState_NoDevice(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
backend.ethernetDevice = nil
|
||||
err = backend.updateEthernetState()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_UpdateWiFiState_NoDevice(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
backend.wifiDevice = nil
|
||||
err = backend.updateWiFiState()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ClassifyNMStateReason(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
reason uint32
|
||||
expected string
|
||||
}{
|
||||
{NmDeviceStateReasonWrongPassword, errdefs.ErrBadCredentials},
|
||||
{NmDeviceStateReasonNoSecrets, errdefs.ErrUserCanceled},
|
||||
{NmDeviceStateReasonSupplicantTimeout, errdefs.ErrBadCredentials},
|
||||
{NmDeviceStateReasonDhcpClientFailed, errdefs.ErrDhcpTimeout},
|
||||
{NmDeviceStateReasonNoSsid, errdefs.ErrNoSuchSSID},
|
||||
{999, errdefs.ErrConnectionFailed},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := backend.classifyNMStateReason(tc.reason)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_GetDeviceIP_NoConfig(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
mockDevice := mock_gonetworkmanager.NewMockDevice(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockDevice.EXPECT().GetPropertyIP4Config().Return(nil, nil)
|
||||
|
||||
ip := backend.getDeviceIP(mockDevice)
|
||||
assert.Empty(t, ip)
|
||||
}
|
||||
154
core/internal/server/network/backend_networkmanager_test.go
Normal file
154
core/internal/server/network/backend_networkmanager_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNetworkManagerBackend_New(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
assert.NotNil(t, backend)
|
||||
assert.Equal(t, "networkmanager", backend.state.Backend)
|
||||
assert.NotNil(t, backend.stopChan)
|
||||
assert.NotNil(t, backend.state)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_GetCurrentState(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.state.NetworkStatus = StatusWiFi
|
||||
backend.state.WiFiConnected = true
|
||||
backend.state.WiFiSSID = "TestNetwork"
|
||||
backend.state.WiFiIP = "192.168.1.100"
|
||||
backend.state.WiFiNetworks = []WiFiNetwork{
|
||||
{SSID: "TestNetwork", Signal: 80, Connected: true},
|
||||
}
|
||||
backend.state.WiredConnections = []WiredConnection{
|
||||
{ID: "Wired connection 1", UUID: "test-uuid"},
|
||||
}
|
||||
|
||||
state, err := backend.GetCurrentState()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, state)
|
||||
assert.Equal(t, StatusWiFi, state.NetworkStatus)
|
||||
assert.True(t, state.WiFiConnected)
|
||||
assert.Equal(t, "TestNetwork", state.WiFiSSID)
|
||||
assert.Len(t, state.WiFiNetworks, 1)
|
||||
assert.Len(t, state.WiredConnections, 1)
|
||||
|
||||
assert.NotSame(t, &backend.state.WiFiNetworks, &state.WiFiNetworks)
|
||||
assert.NotSame(t, &backend.state.WiredConnections, &state.WiredConnections)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_SetPromptBroker_Nil(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
err = backend.SetPromptBroker(nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot be nil")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_SubmitCredentials_NoBroker(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.promptBroker = nil
|
||||
err = backend.SubmitCredentials("token", map[string]string{"password": "test"}, false)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not initialized")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_CancelCredentials_NoBroker(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.promptBroker = nil
|
||||
err = backend.CancelCredentials("token")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not initialized")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_EnsureWiFiDevice_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.wifiDevice = nil
|
||||
backend.wifiDev = nil
|
||||
|
||||
err = backend.ensureWiFiDevice()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_EnsureWiFiDevice_AlreadySet(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.wifiDev = "dummy-device"
|
||||
|
||||
err = backend.ensureWiFiDevice()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_StartSecretAgent_NoBroker(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.promptBroker = nil
|
||||
err = backend.startSecretAgent()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "prompt broker not set")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_Close(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_GetPromptBroker(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
broker := backend.GetPromptBroker()
|
||||
assert.Nil(t, broker)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_StopMonitoring_NoSignals(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.StopMonitoring()
|
||||
})
|
||||
}
|
||||
527
core/internal/server/network/backend_networkmanager_vpn.go
Normal file
527
core/internal/server/network/backend_networkmanager_vpn.go
Normal file
@@ -0,0 +1,527 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
)
|
||||
|
||||
func (b *NetworkManagerBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
var err error
|
||||
s, err = gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settingsMgr := s.(gonetworkmanager.Settings)
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
var profiles []VPNProfile
|
||||
for _, conn := range connections {
|
||||
settings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
connMeta, ok := settings["connection"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
connType, _ := connMeta["type"].(string)
|
||||
if connType != "vpn" && connType != "wireguard" {
|
||||
continue
|
||||
}
|
||||
|
||||
connID, _ := connMeta["id"].(string)
|
||||
connUUID, _ := connMeta["uuid"].(string)
|
||||
|
||||
profile := VPNProfile{
|
||||
Name: connID,
|
||||
UUID: connUUID,
|
||||
Type: connType,
|
||||
}
|
||||
|
||||
if connType == "vpn" {
|
||||
if vpnSettings, ok := settings["vpn"]; ok {
|
||||
if svcType, ok := vpnSettings["service-type"].(string); ok {
|
||||
profile.ServiceType = svcType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
|
||||
sort.Slice(profiles, func(i, j int) bool {
|
||||
return strings.ToLower(profiles[i].Name) < strings.ToLower(profiles[j].Name)
|
||||
})
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.VPNProfiles = profiles
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ListActiveVPN() ([]VPNActive, error) {
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
|
||||
activeConns, err := nm.GetPropertyActiveConnections()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active connections: %w", err)
|
||||
}
|
||||
|
||||
var active []VPNActive
|
||||
for _, activeConn := range activeConns {
|
||||
connType, err := activeConn.GetPropertyType()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connType != "vpn" && connType != "wireguard" {
|
||||
continue
|
||||
}
|
||||
|
||||
uuid, _ := activeConn.GetPropertyUUID()
|
||||
id, _ := activeConn.GetPropertyID()
|
||||
state, _ := activeConn.GetPropertyState()
|
||||
|
||||
var stateStr string
|
||||
switch state {
|
||||
case 0:
|
||||
stateStr = "unknown"
|
||||
case 1:
|
||||
stateStr = "activating"
|
||||
case 2:
|
||||
stateStr = "activated"
|
||||
case 3:
|
||||
stateStr = "deactivating"
|
||||
case 4:
|
||||
stateStr = "deactivated"
|
||||
}
|
||||
|
||||
vpnActive := VPNActive{
|
||||
Name: id,
|
||||
UUID: uuid,
|
||||
State: stateStr,
|
||||
Type: connType,
|
||||
Plugin: "",
|
||||
}
|
||||
|
||||
if connType == "vpn" {
|
||||
conn, _ := activeConn.GetPropertyConnection()
|
||||
if conn != nil {
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err == nil {
|
||||
if vpnSettings, ok := connSettings["vpn"]; ok {
|
||||
if svcType, ok := vpnSettings["service-type"].(string); ok {
|
||||
vpnActive.Plugin = svcType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
active = append(active, vpnActive)
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.VPNActive = active
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
return active, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||
if singleActive {
|
||||
active, err := b.ListActiveVPN()
|
||||
if err == nil && len(active) > 0 {
|
||||
alreadyConnected := false
|
||||
for _, vpn := range active {
|
||||
if vpn.UUID == uuidOrName || vpn.Name == uuidOrName {
|
||||
alreadyConnected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !alreadyConnected {
|
||||
if err := b.DisconnectAllVPN(); err != nil {
|
||||
log.Warnf("Failed to disconnect existing VPNs: %v", err)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
var err error
|
||||
s, err = gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settingsMgr := s.(gonetworkmanager.Settings)
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
var targetConn gonetworkmanager.Connection
|
||||
for _, conn := range connections {
|
||||
settings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
connMeta, ok := settings["connection"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
connType, _ := connMeta["type"].(string)
|
||||
if connType != "vpn" && connType != "wireguard" {
|
||||
continue
|
||||
}
|
||||
|
||||
connID, _ := connMeta["id"].(string)
|
||||
connUUID, _ := connMeta["uuid"].(string)
|
||||
|
||||
if connUUID == uuidOrName || connID == uuidOrName {
|
||||
targetConn = conn
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetConn == nil {
|
||||
return fmt.Errorf("VPN connection not found: %s", uuidOrName)
|
||||
}
|
||||
|
||||
targetSettings, err := targetConn.GetSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connection settings: %w", err)
|
||||
}
|
||||
|
||||
var targetUUID string
|
||||
if connMeta, ok := targetSettings["connection"]; ok {
|
||||
if uuid, ok := connMeta["uuid"].(string); ok {
|
||||
targetUUID = uuid
|
||||
}
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = true
|
||||
b.state.ConnectingVPNUUID = targetUUID
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
activeConn, err := nm.ActivateConnection(targetConn, nil, nil)
|
||||
if err != nil {
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = false
|
||||
b.state.ConnectingVPNUUID = ""
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to activate VPN: %w", err)
|
||||
}
|
||||
|
||||
if activeConn != nil {
|
||||
state, _ := activeConn.GetPropertyState()
|
||||
if state == 2 {
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = false
|
||||
b.state.ConnectingVPNUUID = ""
|
||||
b.stateMutex.Unlock()
|
||||
b.ListActiveVPN()
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) DisconnectVPN(uuidOrName string) error {
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
|
||||
activeConns, err := nm.GetPropertyActiveConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get active connections: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("[DisconnectVPN] Looking for VPN: %s", uuidOrName)
|
||||
|
||||
for _, activeConn := range activeConns {
|
||||
connType, err := activeConn.GetPropertyType()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connType != "vpn" && connType != "wireguard" {
|
||||
continue
|
||||
}
|
||||
|
||||
uuid, _ := activeConn.GetPropertyUUID()
|
||||
id, _ := activeConn.GetPropertyID()
|
||||
state, _ := activeConn.GetPropertyState()
|
||||
|
||||
log.Debugf("[DisconnectVPN] Found active VPN: uuid=%s id=%s state=%d", uuid, id, state)
|
||||
|
||||
if uuid == uuidOrName || id == uuidOrName {
|
||||
log.Infof("[DisconnectVPN] Deactivating VPN: %s (state=%d)", id, state)
|
||||
if err := nm.DeactivateConnection(activeConn); err != nil {
|
||||
return fmt.Errorf("failed to deactivate VPN: %w", err)
|
||||
}
|
||||
b.ListActiveVPN()
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Warnf("[DisconnectVPN] VPN not found in active connections: %s", uuidOrName)
|
||||
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
var err error
|
||||
s, err = gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("VPN connection not active and cannot access settings: %w", err)
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settingsMgr := s.(gonetworkmanager.Settings)
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("VPN connection not active: %s", uuidOrName)
|
||||
}
|
||||
|
||||
for _, conn := range connections {
|
||||
settings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
connMeta, ok := settings["connection"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
connType, _ := connMeta["type"].(string)
|
||||
if connType != "vpn" && connType != "wireguard" {
|
||||
continue
|
||||
}
|
||||
|
||||
connID, _ := connMeta["id"].(string)
|
||||
connUUID, _ := connMeta["uuid"].(string)
|
||||
|
||||
if connUUID == uuidOrName || connID == uuidOrName {
|
||||
log.Infof("[DisconnectVPN] VPN connection exists but not active: %s", connID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("VPN connection not found: %s", uuidOrName)
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) DisconnectAllVPN() error {
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
|
||||
activeConns, err := nm.GetPropertyActiveConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get active connections: %w", err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
var disconnected bool
|
||||
for _, activeConn := range activeConns {
|
||||
connType, err := activeConn.GetPropertyType()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connType != "vpn" && connType != "wireguard" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := nm.DeactivateConnection(activeConn); err != nil {
|
||||
lastErr = err
|
||||
log.Warnf("Failed to deactivate VPN connection: %v", err)
|
||||
} else {
|
||||
disconnected = true
|
||||
}
|
||||
}
|
||||
|
||||
if disconnected {
|
||||
b.ListActiveVPN()
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ClearVPNCredentials(uuidOrName string) error {
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
var err error
|
||||
s, err = gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settingsMgr := s.(gonetworkmanager.Settings)
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
for _, conn := range connections {
|
||||
settings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
connMeta, ok := settings["connection"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
connType, _ := connMeta["type"].(string)
|
||||
if connType != "vpn" && connType != "wireguard" {
|
||||
continue
|
||||
}
|
||||
|
||||
connID, _ := connMeta["id"].(string)
|
||||
connUUID, _ := connMeta["uuid"].(string)
|
||||
|
||||
if connUUID == uuidOrName || connID == uuidOrName {
|
||||
if connType == "vpn" {
|
||||
if vpnSettings, ok := settings["vpn"]; ok {
|
||||
delete(vpnSettings, "secrets")
|
||||
|
||||
if dataMap, ok := vpnSettings["data"].(map[string]string); ok {
|
||||
dataMap["password-flags"] = "1"
|
||||
vpnSettings["data"] = dataMap
|
||||
}
|
||||
|
||||
vpnSettings["password-flags"] = uint32(1)
|
||||
}
|
||||
|
||||
settings["vpn-secrets"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if err := conn.Update(settings); err != nil {
|
||||
return fmt.Errorf("failed to update connection: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.ClearSecrets(); err != nil {
|
||||
log.Warnf("ClearSecrets call failed (may not be critical): %v", err)
|
||||
}
|
||||
|
||||
log.Infof("Cleared credentials for VPN: %s", connID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("VPN connection not found: %s", uuidOrName)
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
||||
b.stateMutex.RLock()
|
||||
isConnectingVPN := b.state.IsConnectingVPN
|
||||
connectingVPNUUID := b.state.ConnectingVPNUUID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
if !isConnectingVPN || connectingVPNUUID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
activeConns, err := nm.GetPropertyActiveConnections()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
foundConnection := false
|
||||
for _, activeConn := range activeConns {
|
||||
connType, err := activeConn.GetPropertyType()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connType != "vpn" && connType != "wireguard" {
|
||||
continue
|
||||
}
|
||||
|
||||
uuid, err := activeConn.GetPropertyUUID()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
state, _ := activeConn.GetPropertyState()
|
||||
stateReason, _ := activeConn.GetPropertyStateFlags()
|
||||
|
||||
if uuid == connectingVPNUUID {
|
||||
foundConnection = true
|
||||
|
||||
switch state {
|
||||
case 2:
|
||||
log.Infof("[updateVPNConnectionState] VPN connection successful: %s", uuid)
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = false
|
||||
b.state.ConnectingVPNUUID = ""
|
||||
b.state.LastError = ""
|
||||
b.stateMutex.Unlock()
|
||||
return
|
||||
case 4:
|
||||
log.Warnf("[updateVPNConnectionState] VPN connection failed/deactivated: %s (state=%d, flags=%d)", uuid, state, stateReason)
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = false
|
||||
b.state.ConnectingVPNUUID = ""
|
||||
b.state.LastError = "VPN connection failed"
|
||||
b.stateMutex.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundConnection {
|
||||
log.Warnf("[updateVPNConnectionState] VPN connection no longer exists: %s", connectingVPNUUID)
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = false
|
||||
b.state.ConnectingVPNUUID = ""
|
||||
b.state.LastError = "VPN connection failed"
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
138
core/internal/server/network/backend_networkmanager_vpn_test.go
Normal file
138
core/internal/server/network/backend_networkmanager_vpn_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNetworkManagerBackend_ListVPNProfiles(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
mockSettings := mock_gonetworkmanager.NewMockSettings(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
backend.settings = mockSettings
|
||||
|
||||
mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{}, nil)
|
||||
|
||||
profiles, err := backend.ListVPNProfiles()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, profiles)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ListActiveVPN(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil)
|
||||
|
||||
active, err := backend.ListActiveVPN()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, active)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ConnectVPN_NotFound(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
mockSettings := mock_gonetworkmanager.NewMockSettings(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
backend.settings = mockSettings
|
||||
|
||||
mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{}, nil)
|
||||
|
||||
err = backend.ConnectVPN("non-existent-vpn-12345", false)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ConnectVPN_SingleActive_NoActiveVPN(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
mockSettings := mock_gonetworkmanager.NewMockSettings(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
backend.settings = mockSettings
|
||||
|
||||
mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{}, nil)
|
||||
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil)
|
||||
|
||||
err = backend.ConnectVPN("non-existent-vpn-12345", true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_DisconnectVPN_NotActive(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil)
|
||||
|
||||
err = backend.DisconnectVPN("non-existent-vpn-12345")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_DisconnectAllVPN(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil)
|
||||
|
||||
err = backend.DisconnectAllVPN()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ClearVPNCredentials_NotFound(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
mockSettings := mock_gonetworkmanager.NewMockSettings(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
backend.settings = mockSettings
|
||||
|
||||
mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{}, nil)
|
||||
|
||||
err = backend.ClearVPNCredentials("non-existent-vpn-12345")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_UpdateVPNConnectionState_NotConnecting(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
backend.stateMutex.Lock()
|
||||
backend.state.IsConnectingVPN = false
|
||||
backend.state.ConnectingVPNUUID = ""
|
||||
backend.stateMutex.Unlock()
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.updateVPNConnectionState()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_UpdateVPNConnectionState_EmptyUUID(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
backend.stateMutex.Lock()
|
||||
backend.state.IsConnectingVPN = true
|
||||
backend.state.ConnectingVPNUUID = ""
|
||||
backend.stateMutex.Unlock()
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
backend.updateVPNConnectionState()
|
||||
})
|
||||
}
|
||||
718
core/internal/server/network/backend_networkmanager_wifi.go
Normal file
718
core/internal/server/network/backend_networkmanager_wifi.go
Normal file
@@ -0,0 +1,718 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
)
|
||||
|
||||
func (b *NetworkManagerBackend) GetWiFiEnabled() (bool, error) {
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
return nm.GetPropertyWirelessEnabled()
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) SetWiFiEnabled(enabled bool) error {
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
err := nm.SetPropertyWirelessEnabled(enabled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set WiFi enabled: %w", err)
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiEnabled = enabled
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ScanWiFi() error {
|
||||
if b.wifiDevice == nil {
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
enabled := b.state.WiFiEnabled
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
if !enabled {
|
||||
return fmt.Errorf("WiFi is disabled")
|
||||
}
|
||||
|
||||
if err := b.ensureWiFiDevice(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := b.wifiDev.(gonetworkmanager.DeviceWireless)
|
||||
err := w.RequestScan()
|
||||
if err != nil {
|
||||
return fmt.Errorf("scan request failed: %w", err)
|
||||
}
|
||||
|
||||
_, err = b.updateWiFiNetworks()
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) {
|
||||
if b.wifiDevice == nil {
|
||||
return nil, fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
if err := b.ensureWiFiDevice(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wifiDev := b.wifiDev
|
||||
|
||||
w := wifiDev.(gonetworkmanager.DeviceWireless)
|
||||
apPaths, err := w.GetAccessPoints()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access points: %w", err)
|
||||
}
|
||||
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
s, err = gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settingsMgr := s.(gonetworkmanager.Settings)
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
savedSSIDs := make(map[string]bool)
|
||||
autoconnectMap := make(map[string]bool)
|
||||
for _, conn := range connections {
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connMeta, ok := connSettings["connection"]; ok {
|
||||
if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" {
|
||||
if wifiSettings, ok := connSettings["802-11-wireless"]; ok {
|
||||
if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok {
|
||||
savedSSID := string(ssidBytes)
|
||||
savedSSIDs[savedSSID] = true
|
||||
autoconnect := true
|
||||
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
||||
autoconnect = ac
|
||||
}
|
||||
autoconnectMap[savedSSID] = autoconnect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
currentSSID := b.state.WiFiSSID
|
||||
currentBSSID := b.state.WiFiBSSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
var bands []WiFiNetwork
|
||||
|
||||
for _, ap := range apPaths {
|
||||
apSSID, err := ap.GetPropertySSID()
|
||||
if err != nil || apSSID != ssid {
|
||||
continue
|
||||
}
|
||||
|
||||
strength, _ := ap.GetPropertyStrength()
|
||||
flags, _ := ap.GetPropertyFlags()
|
||||
wpaFlags, _ := ap.GetPropertyWPAFlags()
|
||||
rsnFlags, _ := ap.GetPropertyRSNFlags()
|
||||
freq, _ := ap.GetPropertyFrequency()
|
||||
maxBitrate, _ := ap.GetPropertyMaxBitrate()
|
||||
bssid, _ := ap.GetPropertyHWAddress()
|
||||
mode, _ := ap.GetPropertyMode()
|
||||
|
||||
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
||||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
||||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
||||
|
||||
enterprise := (rsnFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) ||
|
||||
(wpaFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0)
|
||||
|
||||
var modeStr string
|
||||
switch mode {
|
||||
case gonetworkmanager.Nm80211ModeAdhoc:
|
||||
modeStr = "adhoc"
|
||||
case gonetworkmanager.Nm80211ModeInfra:
|
||||
modeStr = "infrastructure"
|
||||
case gonetworkmanager.Nm80211ModeAp:
|
||||
modeStr = "ap"
|
||||
default:
|
||||
modeStr = "unknown"
|
||||
}
|
||||
|
||||
channel := frequencyToChannel(freq)
|
||||
|
||||
network := WiFiNetwork{
|
||||
SSID: ssid,
|
||||
BSSID: bssid,
|
||||
Signal: strength,
|
||||
Secured: secured,
|
||||
Enterprise: enterprise,
|
||||
Connected: ssid == currentSSID && bssid == currentBSSID,
|
||||
Saved: savedSSIDs[ssid],
|
||||
Autoconnect: autoconnectMap[ssid],
|
||||
Frequency: freq,
|
||||
Mode: modeStr,
|
||||
Rate: maxBitrate / 1000,
|
||||
Channel: channel,
|
||||
}
|
||||
|
||||
bands = append(bands, network)
|
||||
}
|
||||
|
||||
if len(bands) == 0 {
|
||||
return nil, fmt.Errorf("network not found: %s", ssid)
|
||||
}
|
||||
|
||||
sort.Slice(bands, func(i, j int) bool {
|
||||
if bands[i].Connected && !bands[j].Connected {
|
||||
return true
|
||||
}
|
||||
if !bands[i].Connected && bands[j].Connected {
|
||||
return false
|
||||
}
|
||||
return bands[i].Signal > bands[j].Signal
|
||||
})
|
||||
|
||||
return &NetworkInfoResponse{
|
||||
SSID: ssid,
|
||||
Bands: bands,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
if b.wifiDevice == nil {
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
alreadyConnected := b.state.WiFiConnected && b.state.WiFiSSID == req.SSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
if alreadyConnected && !req.Interactive {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnecting = true
|
||||
b.state.ConnectingSSID = req.SSID
|
||||
b.state.LastError = ""
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
|
||||
existingConn, err := b.findConnection(req.SSID)
|
||||
if err == nil && existingConn != nil {
|
||||
dev := b.wifiDevice.(gonetworkmanager.Device)
|
||||
|
||||
_, err := nm.ActivateConnection(existingConn, dev, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[ConnectWiFi] Failed to activate existing connection: %v", err)
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = fmt.Sprintf("failed to activate connection: %v", err)
|
||||
b.stateMutex.Unlock()
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
return fmt.Errorf("failed to activate connection: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := b.createAndConnectWiFi(req); err != nil {
|
||||
log.Warnf("[ConnectWiFi] Failed to create and connect: %v", err)
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = err.Error()
|
||||
b.stateMutex.Unlock()
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) DisconnectWiFi() error {
|
||||
if b.wifiDevice == nil {
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
dev := b.wifiDevice.(gonetworkmanager.Device)
|
||||
|
||||
err := dev.Disconnect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to disconnect: %w", err)
|
||||
}
|
||||
|
||||
b.updateWiFiState()
|
||||
b.updatePrimaryConnection()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ForgetWiFiNetwork(ssid string) error {
|
||||
conn, err := b.findConnection(ssid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection not found: %w", err)
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
currentSSID := b.state.WiFiSSID
|
||||
isConnected := b.state.WiFiConnected
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
err = conn.Delete()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete connection: %w", err)
|
||||
}
|
||||
|
||||
if isConnected && currentSSID == ssid {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiConnected = false
|
||||
b.state.WiFiSSID = ""
|
||||
b.state.WiFiBSSID = ""
|
||||
b.state.WiFiSignal = 0
|
||||
b.state.WiFiIP = ""
|
||||
b.state.NetworkStatus = StatusDisconnected
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
b.updateWiFiNetworks()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) IsConnectingTo(ssid string) bool {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
return b.state.IsConnecting && b.state.ConnectingSSID == ssid
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
||||
if b.wifiDevice == nil {
|
||||
return nil, fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
if err := b.ensureWiFiDevice(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wifiDev := b.wifiDev
|
||||
|
||||
w := wifiDev.(gonetworkmanager.DeviceWireless)
|
||||
apPaths, err := w.GetAccessPoints()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access points: %w", err)
|
||||
}
|
||||
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
s, err = gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settingsMgr := s.(gonetworkmanager.Settings)
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
savedSSIDs := make(map[string]bool)
|
||||
autoconnectMap := make(map[string]bool)
|
||||
for _, conn := range connections {
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connMeta, ok := connSettings["connection"]; ok {
|
||||
if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" {
|
||||
if wifiSettings, ok := connSettings["802-11-wireless"]; ok {
|
||||
if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok {
|
||||
ssid := string(ssidBytes)
|
||||
savedSSIDs[ssid] = true
|
||||
autoconnect := true
|
||||
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
||||
autoconnect = ac
|
||||
}
|
||||
autoconnectMap[ssid] = autoconnect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.stateMutex.RLock()
|
||||
currentSSID := b.state.WiFiSSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
seenSSIDs := make(map[string]*WiFiNetwork)
|
||||
networks := []WiFiNetwork{}
|
||||
|
||||
for _, ap := range apPaths {
|
||||
ssid, err := ap.GetPropertySSID()
|
||||
if err != nil || ssid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if existing, exists := seenSSIDs[ssid]; exists {
|
||||
strength, _ := ap.GetPropertyStrength()
|
||||
if strength > existing.Signal {
|
||||
existing.Signal = strength
|
||||
freq, _ := ap.GetPropertyFrequency()
|
||||
existing.Frequency = freq
|
||||
bssid, _ := ap.GetPropertyHWAddress()
|
||||
existing.BSSID = bssid
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
strength, _ := ap.GetPropertyStrength()
|
||||
flags, _ := ap.GetPropertyFlags()
|
||||
wpaFlags, _ := ap.GetPropertyWPAFlags()
|
||||
rsnFlags, _ := ap.GetPropertyRSNFlags()
|
||||
freq, _ := ap.GetPropertyFrequency()
|
||||
maxBitrate, _ := ap.GetPropertyMaxBitrate()
|
||||
bssid, _ := ap.GetPropertyHWAddress()
|
||||
mode, _ := ap.GetPropertyMode()
|
||||
|
||||
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
||||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
||||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
||||
|
||||
enterprise := (rsnFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) ||
|
||||
(wpaFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0)
|
||||
|
||||
var modeStr string
|
||||
switch mode {
|
||||
case gonetworkmanager.Nm80211ModeAdhoc:
|
||||
modeStr = "adhoc"
|
||||
case gonetworkmanager.Nm80211ModeInfra:
|
||||
modeStr = "infrastructure"
|
||||
case gonetworkmanager.Nm80211ModeAp:
|
||||
modeStr = "ap"
|
||||
default:
|
||||
modeStr = "unknown"
|
||||
}
|
||||
|
||||
channel := frequencyToChannel(freq)
|
||||
|
||||
network := WiFiNetwork{
|
||||
SSID: ssid,
|
||||
BSSID: bssid,
|
||||
Signal: strength,
|
||||
Secured: secured,
|
||||
Enterprise: enterprise,
|
||||
Connected: ssid == currentSSID,
|
||||
Saved: savedSSIDs[ssid],
|
||||
Autoconnect: autoconnectMap[ssid],
|
||||
Frequency: freq,
|
||||
Mode: modeStr,
|
||||
Rate: maxBitrate / 1000,
|
||||
Channel: channel,
|
||||
}
|
||||
|
||||
seenSSIDs[ssid] = &network
|
||||
networks = append(networks, network)
|
||||
}
|
||||
|
||||
sortWiFiNetworks(networks)
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiNetworks = networks
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) findConnection(ssid string) (gonetworkmanager.Connection, error) {
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
var err error
|
||||
s, err = gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settings := s.(gonetworkmanager.Settings)
|
||||
connections, err := settings.ListConnections()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ssidBytes := []byte(ssid)
|
||||
for _, conn := range connections {
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connMeta, ok := connSettings["connection"]; ok {
|
||||
if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" {
|
||||
if wifiSettings, ok := connSettings["802-11-wireless"]; ok {
|
||||
if candidateSSID, ok := wifiSettings["ssid"].([]byte); ok {
|
||||
if bytes.Equal(candidateSSID, ssidBytes) {
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("connection not found")
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) createAndConnectWiFi(req ConnectionRequest) error {
|
||||
if b.wifiDevice == nil {
|
||||
return fmt.Errorf("no WiFi device available")
|
||||
}
|
||||
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
dev := b.wifiDevice.(gonetworkmanager.Device)
|
||||
|
||||
if err := b.ensureWiFiDevice(); err != nil {
|
||||
return err
|
||||
}
|
||||
wifiDev := b.wifiDev
|
||||
|
||||
w := wifiDev.(gonetworkmanager.DeviceWireless)
|
||||
apPaths, err := w.GetAccessPoints()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get access points: %w", err)
|
||||
}
|
||||
|
||||
var targetAP gonetworkmanager.AccessPoint
|
||||
for _, ap := range apPaths {
|
||||
ssid, err := ap.GetPropertySSID()
|
||||
if err != nil || ssid != req.SSID {
|
||||
continue
|
||||
}
|
||||
targetAP = ap
|
||||
break
|
||||
}
|
||||
|
||||
if targetAP == nil {
|
||||
return fmt.Errorf("access point not found: %s", req.SSID)
|
||||
}
|
||||
|
||||
flags, _ := targetAP.GetPropertyFlags()
|
||||
wpaFlags, _ := targetAP.GetPropertyWPAFlags()
|
||||
rsnFlags, _ := targetAP.GetPropertyRSNFlags()
|
||||
|
||||
const KeyMgmt8021x = uint32(512)
|
||||
const KeyMgmtPsk = uint32(256)
|
||||
const KeyMgmtSae = uint32(1024)
|
||||
|
||||
isEnterprise := (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
|
||||
isPsk := (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
|
||||
isSae := (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
|
||||
|
||||
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
||||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
||||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
||||
|
||||
if isEnterprise {
|
||||
log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v",
|
||||
req.SSID, req.Interactive)
|
||||
}
|
||||
|
||||
settings := make(map[string]map[string]interface{})
|
||||
|
||||
settings["connection"] = map[string]interface{}{
|
||||
"id": req.SSID,
|
||||
"type": "802-11-wireless",
|
||||
"autoconnect": true,
|
||||
}
|
||||
|
||||
settings["ipv4"] = map[string]interface{}{"method": "auto"}
|
||||
settings["ipv6"] = map[string]interface{}{"method": "auto"}
|
||||
|
||||
if secured {
|
||||
settings["802-11-wireless"] = map[string]interface{}{
|
||||
"ssid": []byte(req.SSID),
|
||||
"mode": "infrastructure",
|
||||
"security": "802-11-wireless-security",
|
||||
}
|
||||
|
||||
switch {
|
||||
case isEnterprise || req.Username != "":
|
||||
settings["802-11-wireless-security"] = map[string]interface{}{
|
||||
"key-mgmt": "wpa-eap",
|
||||
}
|
||||
|
||||
x := map[string]interface{}{
|
||||
"eap": []string{"peap"},
|
||||
"phase2-auth": "mschapv2",
|
||||
"system-ca-certs": false,
|
||||
"password-flags": uint32(0),
|
||||
}
|
||||
|
||||
if req.Username != "" {
|
||||
x["identity"] = req.Username
|
||||
}
|
||||
if req.Password != "" {
|
||||
x["password"] = req.Password
|
||||
}
|
||||
|
||||
if req.AnonymousIdentity != "" {
|
||||
x["anonymous-identity"] = req.AnonymousIdentity
|
||||
}
|
||||
if req.DomainSuffixMatch != "" {
|
||||
x["domain-suffix-match"] = req.DomainSuffixMatch
|
||||
}
|
||||
|
||||
settings["802-1x"] = x
|
||||
|
||||
log.Infof("[createAndConnectWiFi] WPA-EAP settings: eap=peap, phase2-auth=mschapv2, identity=%s, interactive=%v, system-ca-certs=%v, domain-suffix-match=%q",
|
||||
req.Username, req.Interactive, x["system-ca-certs"], req.DomainSuffixMatch)
|
||||
|
||||
case isPsk:
|
||||
sec := map[string]interface{}{
|
||||
"key-mgmt": "wpa-psk",
|
||||
"psk-flags": uint32(0),
|
||||
}
|
||||
if !req.Interactive {
|
||||
sec["psk"] = req.Password
|
||||
}
|
||||
settings["802-11-wireless-security"] = sec
|
||||
|
||||
case isSae:
|
||||
sec := map[string]interface{}{
|
||||
"key-mgmt": "sae",
|
||||
"pmf": int32(3),
|
||||
"psk-flags": uint32(0),
|
||||
}
|
||||
if !req.Interactive {
|
||||
sec["psk"] = req.Password
|
||||
}
|
||||
settings["802-11-wireless-security"] = sec
|
||||
|
||||
default:
|
||||
return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags)
|
||||
}
|
||||
} else {
|
||||
settings["802-11-wireless"] = map[string]interface{}{
|
||||
"ssid": []byte(req.SSID),
|
||||
"mode": "infrastructure",
|
||||
}
|
||||
}
|
||||
|
||||
if req.Interactive {
|
||||
s := b.settings
|
||||
if s == nil {
|
||||
var settingsErr error
|
||||
s, settingsErr = gonetworkmanager.NewSettings()
|
||||
if settingsErr != nil {
|
||||
return fmt.Errorf("failed to get settings manager: %w", settingsErr)
|
||||
}
|
||||
b.settings = s
|
||||
}
|
||||
|
||||
settingsMgr := s.(gonetworkmanager.Settings)
|
||||
conn, err := settingsMgr.AddConnection(settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add connection: %w", err)
|
||||
}
|
||||
|
||||
if isEnterprise {
|
||||
log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)")
|
||||
}
|
||||
|
||||
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to activate connection: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
||||
} else {
|
||||
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
|
||||
conn, err := b.findConnection(ssid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection not found: %w", err)
|
||||
}
|
||||
|
||||
settings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connection settings: %w", err)
|
||||
}
|
||||
|
||||
if connMeta, ok := settings["connection"]; ok {
|
||||
connMeta["autoconnect"] = autoconnect
|
||||
} else {
|
||||
return fmt.Errorf("connection metadata not found")
|
||||
}
|
||||
|
||||
if ipv4, ok := settings["ipv4"]; ok {
|
||||
delete(ipv4, "addresses")
|
||||
delete(ipv4, "routes")
|
||||
delete(ipv4, "dns")
|
||||
}
|
||||
|
||||
if ipv6, ok := settings["ipv6"]; ok {
|
||||
delete(ipv6, "addresses")
|
||||
delete(ipv6, "routes")
|
||||
delete(ipv6, "dns")
|
||||
}
|
||||
|
||||
err = conn.Update(settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update connection: %w", err)
|
||||
}
|
||||
|
||||
b.updateWiFiNetworks()
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
198
core/internal/server/network/backend_networkmanager_wifi_test.go
Normal file
198
core/internal/server/network/backend_networkmanager_wifi_test.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNetworkManagerBackend_GetWiFiEnabled(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockNM.EXPECT().GetPropertyWirelessEnabled().Return(true, nil)
|
||||
|
||||
enabled, err := backend.GetWiFiEnabled()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, enabled)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_SetWiFiEnabled(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
originalState, err := backend.GetWiFiEnabled()
|
||||
if err != nil {
|
||||
t.Skipf("Cannot get WiFi state: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
backend.SetWiFiEnabled(originalState)
|
||||
}()
|
||||
|
||||
err = backend.SetWiFiEnabled(!originalState)
|
||||
assert.NoError(t, err)
|
||||
|
||||
backend.stateMutex.RLock()
|
||||
assert.Equal(t, !originalState, backend.state.WiFiEnabled)
|
||||
backend.stateMutex.RUnlock()
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ScanWiFi_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.wifiDevice = nil
|
||||
err = backend.ScanWiFi()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ScanWiFi_Disabled(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
if backend.wifiDevice == nil {
|
||||
t.Skip("No WiFi device available")
|
||||
}
|
||||
|
||||
backend.stateMutex.Lock()
|
||||
backend.state.WiFiEnabled = false
|
||||
backend.stateMutex.Unlock()
|
||||
|
||||
err = backend.ScanWiFi()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "WiFi is disabled")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_GetWiFiNetworkDetails_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.wifiDevice = nil
|
||||
_, err = backend.GetWiFiNetworkDetails("TestNetwork")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ConnectWiFi_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.wifiDevice = nil
|
||||
req := ConnectionRequest{SSID: "TestNetwork", Password: "password"}
|
||||
err = backend.ConnectWiFi(req)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_ConnectWiFi_AlreadyConnected(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
if backend.wifiDevice == nil {
|
||||
t.Skip("No WiFi device available")
|
||||
}
|
||||
|
||||
backend.stateMutex.Lock()
|
||||
backend.state.WiFiConnected = true
|
||||
backend.state.WiFiSSID = "TestNetwork"
|
||||
backend.stateMutex.Unlock()
|
||||
|
||||
req := ConnectionRequest{SSID: "TestNetwork", Password: "password"}
|
||||
err = backend.ConnectWiFi(req)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_DisconnectWiFi_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.wifiDevice = nil
|
||||
err = backend.DisconnectWiFi()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_IsConnectingTo(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.stateMutex.Lock()
|
||||
backend.state.IsConnecting = true
|
||||
backend.state.ConnectingSSID = "TestNetwork"
|
||||
backend.stateMutex.Unlock()
|
||||
|
||||
assert.True(t, backend.IsConnectingTo("TestNetwork"))
|
||||
assert.False(t, backend.IsConnectingTo("OtherNetwork"))
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_IsConnectingTo_NotConnecting(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.stateMutex.Lock()
|
||||
backend.state.IsConnecting = false
|
||||
backend.state.ConnectingSSID = ""
|
||||
backend.stateMutex.Unlock()
|
||||
|
||||
assert.False(t, backend.IsConnectingTo("TestNetwork"))
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_UpdateWiFiNetworks_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.wifiDevice = nil
|
||||
_, err = backend.updateWiFiNetworks()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_FindConnection_NoSettings(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.settings = nil
|
||||
_, err = backend.findConnection("NonExistentNetwork")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNetworkManagerBackend_CreateAndConnectWiFi_NoDevice(t *testing.T) {
|
||||
backend, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
t.Skipf("NetworkManager not available: %v", err)
|
||||
}
|
||||
|
||||
backend.wifiDevice = nil
|
||||
backend.wifiDev = nil
|
||||
req := ConnectionRequest{SSID: "TestNetwork", Password: "password"}
|
||||
err = backend.createAndConnectWiFi(req)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
22
core/internal/server/network/broker.go
Normal file
22
core/internal/server/network/broker.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
type PromptBroker interface {
|
||||
Ask(ctx context.Context, req PromptRequest) (token string, err error)
|
||||
Wait(ctx context.Context, token string) (PromptReply, error)
|
||||
Resolve(token string, reply PromptReply) error
|
||||
Cancel(path string, setting string) error
|
||||
}
|
||||
|
||||
func generateToken() (string, error) {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
109
core/internal/server/network/connection_test.go
Normal file
109
core/internal/server/network/connection_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package network_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
mocks_network "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/network"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConnectionRequest_Validation(t *testing.T) {
|
||||
t.Run("basic WiFi connection", func(t *testing.T) {
|
||||
req := network.ConnectionRequest{
|
||||
SSID: "TestNetwork",
|
||||
Password: "testpass123",
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, req.SSID)
|
||||
assert.NotEmpty(t, req.Password)
|
||||
assert.Empty(t, req.Username)
|
||||
})
|
||||
|
||||
t.Run("enterprise WiFi connection", func(t *testing.T) {
|
||||
req := network.ConnectionRequest{
|
||||
SSID: "EnterpriseNetwork",
|
||||
Password: "testpass123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, req.SSID)
|
||||
assert.NotEmpty(t, req.Password)
|
||||
assert.NotEmpty(t, req.Username)
|
||||
})
|
||||
|
||||
t.Run("open WiFi connection", func(t *testing.T) {
|
||||
req := network.ConnectionRequest{
|
||||
SSID: "OpenNetwork",
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, req.SSID)
|
||||
assert.Empty(t, req.Password)
|
||||
assert.Empty(t, req.Username)
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_ConnectWiFi_NoDevice(t *testing.T) {
|
||||
backend := mocks_network.NewMockBackend(t)
|
||||
req := network.ConnectionRequest{
|
||||
SSID: "TestNetwork",
|
||||
Password: "testpass123",
|
||||
}
|
||||
backend.EXPECT().ConnectWiFi(req).Return(errors.New("no WiFi device available"))
|
||||
|
||||
manager := network.NewTestManager(backend, &network.NetworkState{})
|
||||
|
||||
err := manager.ConnectWiFi(req)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
|
||||
func TestManager_DisconnectWiFi_NoDevice(t *testing.T) {
|
||||
backend := mocks_network.NewMockBackend(t)
|
||||
backend.EXPECT().DisconnectWiFi().Return(errors.New("no WiFi device available"))
|
||||
|
||||
manager := network.NewTestManager(backend, &network.NetworkState{})
|
||||
|
||||
err := manager.DisconnectWiFi()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no WiFi device available")
|
||||
}
|
||||
|
||||
func TestManager_ForgetWiFiNetwork_NotFound(t *testing.T) {
|
||||
backend := mocks_network.NewMockBackend(t)
|
||||
backend.EXPECT().ForgetWiFiNetwork("NonExistentNetwork").Return(errors.New("connection not found"))
|
||||
|
||||
manager := network.NewTestManager(backend, &network.NetworkState{})
|
||||
|
||||
err := manager.ForgetWiFiNetwork("NonExistentNetwork")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "connection not found")
|
||||
}
|
||||
|
||||
func TestManager_ConnectEthernet_NoDevice(t *testing.T) {
|
||||
backend := mocks_network.NewMockBackend(t)
|
||||
backend.EXPECT().ConnectEthernet().Return(errors.New("no ethernet device available"))
|
||||
|
||||
manager := network.NewTestManager(backend, &network.NetworkState{})
|
||||
|
||||
err := manager.ConnectEthernet()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
||||
}
|
||||
|
||||
func TestManager_DisconnectEthernet_NoDevice(t *testing.T) {
|
||||
backend := mocks_network.NewMockBackend(t)
|
||||
backend.EXPECT().DisconnectEthernet().Return(errors.New("no ethernet device available"))
|
||||
|
||||
manager := network.NewTestManager(backend, &network.NetworkState{})
|
||||
|
||||
err := manager.DisconnectEthernet()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
||||
}
|
||||
|
||||
// Note: More comprehensive tests for connection operations would require
|
||||
// mocking the NetworkManager D-Bus interfaces, which is beyond the scope
|
||||
// of these unit tests. The tests above cover the basic error cases and
|
||||
// validation logic. Integration tests would be needed for full coverage.
|
||||
89
core/internal/server/network/detect.go
Normal file
89
core/internal/server/network/detect.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type BackendType int
|
||||
|
||||
const (
|
||||
BackendNone BackendType = iota
|
||||
BackendNetworkManager
|
||||
BackendIwd
|
||||
BackendConnMan
|
||||
BackendNetworkd
|
||||
)
|
||||
|
||||
func nameHasOwner(bus *dbus.Conn, name string) (bool, error) {
|
||||
obj := bus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
|
||||
var owned bool
|
||||
if err := obj.Call("org.freedesktop.DBus.NameHasOwner", 0, name).Store(&owned); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return owned, nil
|
||||
}
|
||||
|
||||
type DetectResult struct {
|
||||
Backend BackendType
|
||||
HasNM bool
|
||||
HasIwd bool
|
||||
HasConnMan bool
|
||||
HasWpaSupp bool
|
||||
HasNetworkd bool
|
||||
ChosenReason string
|
||||
}
|
||||
|
||||
func DetectNetworkStack() (*DetectResult, error) {
|
||||
bus, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect system bus: %w", err)
|
||||
}
|
||||
defer bus.Close()
|
||||
|
||||
hasNM, _ := nameHasOwner(bus, "org.freedesktop.NetworkManager")
|
||||
hasIwd, _ := nameHasOwner(bus, "net.connman.iwd")
|
||||
hasConn, _ := nameHasOwner(bus, "net.connman")
|
||||
hasWpa, _ := nameHasOwner(bus, "fi.w1.wpa_supplicant1")
|
||||
hasNetworkd, _ := nameHasOwner(bus, "org.freedesktop.network1")
|
||||
|
||||
res := &DetectResult{
|
||||
HasNM: hasNM,
|
||||
HasIwd: hasIwd,
|
||||
HasConnMan: hasConn,
|
||||
HasWpaSupp: hasWpa,
|
||||
HasNetworkd: hasNetworkd,
|
||||
}
|
||||
|
||||
switch {
|
||||
case hasNM:
|
||||
res.Backend = BackendNetworkManager
|
||||
if hasIwd {
|
||||
res.ChosenReason = "NetworkManager present; iwd also running (likely NM's Wi-Fi backend). Using NM API."
|
||||
} else {
|
||||
res.ChosenReason = "NetworkManager present. Using NM API."
|
||||
}
|
||||
case hasConn && hasIwd:
|
||||
res.Backend = BackendConnMan
|
||||
res.ChosenReason = "ConnMan + iwd detected. Use ConnMan API (iwd is its Wi-Fi daemon)."
|
||||
case hasIwd && hasNetworkd:
|
||||
res.Backend = BackendNetworkd
|
||||
res.ChosenReason = "iwd + systemd-networkd detected. Using iwd for Wi-Fi association and networkd for IP/DHCP."
|
||||
case hasIwd:
|
||||
res.Backend = BackendIwd
|
||||
res.ChosenReason = "iwd detected without NM/ConnMan. Using iwd API."
|
||||
case hasNetworkd:
|
||||
res.Backend = BackendNetworkd
|
||||
res.ChosenReason = "systemd-networkd detected (no NM/ConnMan). Using networkd for L3 and wired."
|
||||
default:
|
||||
res.Backend = BackendNone
|
||||
if hasWpa {
|
||||
res.ChosenReason = "No NM/ConnMan/iwd; wpa_supplicant present. Consider a wpa_supplicant path."
|
||||
} else {
|
||||
res.ChosenReason = "No known network manager bus names found."
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
34
core/internal/server/network/detect_test.go
Normal file
34
core/internal/server/network/detect_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBackendType_Constants(t *testing.T) {
|
||||
assert.Equal(t, BackendType(0), BackendNone)
|
||||
assert.Equal(t, BackendType(1), BackendNetworkManager)
|
||||
assert.Equal(t, BackendType(2), BackendIwd)
|
||||
assert.Equal(t, BackendType(3), BackendConnMan)
|
||||
assert.Equal(t, BackendType(4), BackendNetworkd)
|
||||
}
|
||||
|
||||
func TestDetectResult_HasNetworkdField(t *testing.T) {
|
||||
result := &DetectResult{
|
||||
Backend: BackendNetworkd,
|
||||
HasNetworkd: true,
|
||||
HasIwd: true,
|
||||
}
|
||||
|
||||
assert.True(t, result.HasNetworkd)
|
||||
assert.True(t, result.HasIwd)
|
||||
assert.Equal(t, BackendNetworkd, result.Backend)
|
||||
}
|
||||
|
||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||
result, err := DetectNetworkStack()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
}
|
||||
487
core/internal/server/network/handlers.go
Normal file
487
core/internal/server/network/handlers.go
Normal file
@@ -0,0 +1,487 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type SuccessResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "network.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
case "network.wifi.scan":
|
||||
handleScanWiFi(conn, req, manager)
|
||||
case "network.wifi.networks":
|
||||
handleGetWiFiNetworks(conn, req, manager)
|
||||
case "network.wifi.connect":
|
||||
handleConnectWiFi(conn, req, manager)
|
||||
case "network.wifi.disconnect":
|
||||
handleDisconnectWiFi(conn, req, manager)
|
||||
case "network.wifi.forget":
|
||||
handleForgetWiFi(conn, req, manager)
|
||||
case "network.wifi.toggle":
|
||||
handleToggleWiFi(conn, req, manager)
|
||||
case "network.wifi.enable":
|
||||
handleEnableWiFi(conn, req, manager)
|
||||
case "network.wifi.disable":
|
||||
handleDisableWiFi(conn, req, manager)
|
||||
case "network.ethernet.connect.config":
|
||||
handleConnectEthernetSpecificConfig(conn, req, manager)
|
||||
case "network.ethernet.connect":
|
||||
handleConnectEthernet(conn, req, manager)
|
||||
case "network.ethernet.disconnect":
|
||||
handleDisconnectEthernet(conn, req, manager)
|
||||
case "network.preference.set":
|
||||
handleSetPreference(conn, req, manager)
|
||||
case "network.info":
|
||||
handleGetNetworkInfo(conn, req, manager)
|
||||
case "network.ethernet.info":
|
||||
handleGetWiredNetworkInfo(conn, req, manager)
|
||||
case "network.subscribe":
|
||||
handleSubscribe(conn, req, manager)
|
||||
case "network.credentials.submit":
|
||||
handleCredentialsSubmit(conn, req, manager)
|
||||
case "network.credentials.cancel":
|
||||
handleCredentialsCancel(conn, req, manager)
|
||||
case "network.vpn.profiles":
|
||||
handleListVPNProfiles(conn, req, manager)
|
||||
case "network.vpn.active":
|
||||
handleListActiveVPN(conn, req, manager)
|
||||
case "network.vpn.connect":
|
||||
handleConnectVPN(conn, req, manager)
|
||||
case "network.vpn.disconnect":
|
||||
handleDisconnectVPN(conn, req, manager)
|
||||
case "network.vpn.disconnectAll":
|
||||
handleDisconnectAllVPN(conn, req, manager)
|
||||
case "network.vpn.clearCredentials":
|
||||
handleClearVPNCredentials(conn, req, manager)
|
||||
case "network.wifi.setAutoconnect":
|
||||
handleSetWiFiAutoconnect(conn, req, manager)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleCredentialsSubmit(conn net.Conn, req Request, manager *Manager) {
|
||||
token, ok := req.Params["token"].(string)
|
||||
if !ok {
|
||||
log.Warnf("handleCredentialsSubmit: missing or invalid token parameter")
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
secretsRaw, ok := req.Params["secrets"].(map[string]interface{})
|
||||
if !ok {
|
||||
log.Warnf("handleCredentialsSubmit: missing or invalid secrets parameter")
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'secrets' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
secrets := make(map[string]string)
|
||||
for k, v := range secretsRaw {
|
||||
if str, ok := v.(string); ok {
|
||||
secrets[k] = str
|
||||
}
|
||||
}
|
||||
|
||||
save := true
|
||||
if saveParam, ok := req.Params["save"].(bool); ok {
|
||||
save = saveParam
|
||||
}
|
||||
|
||||
if err := manager.SubmitCredentials(token, secrets, save); err != nil {
|
||||
log.Warnf("handleCredentialsSubmit: failed to submit credentials: %v", err)
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("handleCredentialsSubmit: credentials submitted successfully")
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "credentials submitted"})
|
||||
}
|
||||
|
||||
func handleCredentialsCancel(conn net.Conn, req Request, manager *Manager) {
|
||||
token, ok := req.Params["token"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.CancelCredentials(token); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "credentials cancelled"})
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req Request, manager *Manager) {
|
||||
state := manager.GetState()
|
||||
models.Respond(conn, req.ID, state)
|
||||
}
|
||||
|
||||
func handleScanWiFi(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.ScanWiFi(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "scanning"})
|
||||
}
|
||||
|
||||
func handleGetWiFiNetworks(conn net.Conn, req Request, manager *Manager) {
|
||||
networks := manager.GetWiFiNetworks()
|
||||
models.Respond(conn, req.ID, networks)
|
||||
}
|
||||
|
||||
func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
|
||||
ssid, ok := req.Params["ssid"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
var connReq ConnectionRequest
|
||||
connReq.SSID = ssid
|
||||
|
||||
if password, ok := req.Params["password"].(string); ok {
|
||||
connReq.Password = password
|
||||
}
|
||||
if username, ok := req.Params["username"].(string); ok {
|
||||
connReq.Username = username
|
||||
}
|
||||
|
||||
if interactive, ok := req.Params["interactive"].(bool); ok {
|
||||
connReq.Interactive = interactive
|
||||
} else {
|
||||
state := manager.GetState()
|
||||
alreadyConnected := state.WiFiConnected && state.WiFiSSID == ssid
|
||||
|
||||
if alreadyConnected {
|
||||
connReq.Interactive = false
|
||||
} else {
|
||||
networkInfo, err := manager.GetNetworkInfo(ssid)
|
||||
isSaved := err == nil && networkInfo.Saved
|
||||
|
||||
if isSaved {
|
||||
connReq.Interactive = false
|
||||
} else if err == nil && networkInfo.Secured && connReq.Password == "" && connReq.Username == "" {
|
||||
connReq.Interactive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if anonymousIdentity, ok := req.Params["anonymousIdentity"].(string); ok {
|
||||
connReq.AnonymousIdentity = anonymousIdentity
|
||||
}
|
||||
if domainSuffixMatch, ok := req.Params["domainSuffixMatch"].(string); ok {
|
||||
connReq.DomainSuffixMatch = domainSuffixMatch
|
||||
}
|
||||
|
||||
if err := manager.ConnectWiFi(connReq); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"})
|
||||
}
|
||||
|
||||
func handleDisconnectWiFi(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.DisconnectWiFi(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"})
|
||||
}
|
||||
|
||||
func handleForgetWiFi(conn net.Conn, req Request, manager *Manager) {
|
||||
ssid, ok := req.Params["ssid"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.ForgetWiFiNetwork(ssid); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "forgotten"})
|
||||
}
|
||||
|
||||
func handleToggleWiFi(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.ToggleWiFi(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
state := manager.GetState()
|
||||
models.Respond(conn, req.ID, map[string]bool{"enabled": state.WiFiEnabled})
|
||||
}
|
||||
|
||||
func handleEnableWiFi(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.EnableWiFi(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, map[string]bool{"enabled": true})
|
||||
}
|
||||
|
||||
func handleDisableWiFi(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.DisableWiFi(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, map[string]bool{"enabled": false})
|
||||
}
|
||||
|
||||
func handleConnectEthernetSpecificConfig(conn net.Conn, req Request, manager *Manager) {
|
||||
uuid, ok := req.Params["uuid"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'uuid' parameter")
|
||||
return
|
||||
}
|
||||
if err := manager.activateConnection(uuid); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"})
|
||||
}
|
||||
|
||||
func handleConnectEthernet(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.ConnectEthernet(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"})
|
||||
}
|
||||
|
||||
func handleDisconnectEthernet(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.DisconnectEthernet(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"})
|
||||
}
|
||||
|
||||
func handleSetPreference(conn net.Conn, req Request, manager *Manager) {
|
||||
preference, ok := req.Params["preference"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'preference' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetConnectionPreference(ConnectionPreference(preference)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, map[string]string{"preference": preference})
|
||||
}
|
||||
|
||||
func handleGetNetworkInfo(conn net.Conn, req Request, manager *Manager) {
|
||||
ssid, ok := req.Params["ssid"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
network, err := manager.GetNetworkInfoDetailed(ssid)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, network)
|
||||
}
|
||||
|
||||
func handleGetWiredNetworkInfo(conn net.Conn, req Request, manager *Manager) {
|
||||
uuid, ok := req.Params["uuid"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'uuid' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
network, err := manager.GetWiredNetworkInfoDetailed(uuid)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, network)
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
|
||||
initialState := manager.GetState()
|
||||
event := NetworkEvent{
|
||||
Type: EventStateChanged,
|
||||
Data: initialState,
|
||||
}
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[NetworkEvent]{
|
||||
ID: req.ID,
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for state := range stateChan {
|
||||
event := NetworkEvent{
|
||||
Type: EventStateChanged,
|
||||
Data: state,
|
||||
}
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[NetworkEvent]{
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleListVPNProfiles(conn net.Conn, req Request, manager *Manager) {
|
||||
profiles, err := manager.ListVPNProfiles()
|
||||
if err != nil {
|
||||
log.Warnf("handleListVPNProfiles: failed to list profiles: %v", err)
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list VPN profiles: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, profiles)
|
||||
}
|
||||
|
||||
func handleListActiveVPN(conn net.Conn, req Request, manager *Manager) {
|
||||
active, err := manager.ListActiveVPN()
|
||||
if err != nil {
|
||||
log.Warnf("handleListActiveVPN: failed to list active VPNs: %v", err)
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list active VPNs: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, active)
|
||||
}
|
||||
|
||||
func handleConnectVPN(conn net.Conn, req Request, manager *Manager) {
|
||||
uuidOrName, ok := req.Params["uuidOrName"].(string)
|
||||
if !ok {
|
||||
name, nameOk := req.Params["name"].(string)
|
||||
uuid, uuidOk := req.Params["uuid"].(string)
|
||||
if nameOk {
|
||||
uuidOrName = name
|
||||
} else if uuidOk {
|
||||
uuidOrName = uuid
|
||||
} else {
|
||||
log.Warnf("handleConnectVPN: missing uuidOrName/name/uuid parameter")
|
||||
models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Default to true - only allow one VPN connection at a time
|
||||
singleActive := true
|
||||
if sa, ok := req.Params["singleActive"].(bool); ok {
|
||||
singleActive = sa
|
||||
}
|
||||
|
||||
if err := manager.ConnectVPN(uuidOrName, singleActive); err != nil {
|
||||
log.Warnf("handleConnectVPN: failed to connect: %v", err)
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to connect VPN: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN connection initiated"})
|
||||
}
|
||||
|
||||
func handleDisconnectVPN(conn net.Conn, req Request, manager *Manager) {
|
||||
uuidOrName, ok := req.Params["uuidOrName"].(string)
|
||||
if !ok {
|
||||
name, nameOk := req.Params["name"].(string)
|
||||
uuid, uuidOk := req.Params["uuid"].(string)
|
||||
if nameOk {
|
||||
uuidOrName = name
|
||||
} else if uuidOk {
|
||||
uuidOrName = uuid
|
||||
} else {
|
||||
log.Warnf("handleDisconnectVPN: missing uuidOrName/name/uuid parameter")
|
||||
models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := manager.DisconnectVPN(uuidOrName); err != nil {
|
||||
log.Warnf("handleDisconnectVPN: failed to disconnect: %v", err)
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect VPN: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN disconnected"})
|
||||
}
|
||||
|
||||
func handleDisconnectAllVPN(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.DisconnectAllVPN(); err != nil {
|
||||
log.Warnf("handleDisconnectAllVPN: failed: %v", err)
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect all VPNs: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "All VPNs disconnected"})
|
||||
}
|
||||
|
||||
func handleClearVPNCredentials(conn net.Conn, req Request, manager *Manager) {
|
||||
uuidOrName, ok := req.Params["uuid"].(string)
|
||||
if !ok {
|
||||
uuidOrName, ok = req.Params["name"].(string)
|
||||
}
|
||||
if !ok {
|
||||
uuidOrName, ok = req.Params["uuidOrName"].(string)
|
||||
}
|
||||
if !ok {
|
||||
log.Warnf("handleClearVPNCredentials: missing uuidOrName/name/uuid parameter")
|
||||
models.RespondError(conn, req.ID, "missing uuidOrName/name/uuid parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.ClearVPNCredentials(uuidOrName); err != nil {
|
||||
log.Warnf("handleClearVPNCredentials: failed: %v", err)
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to clear VPN credentials: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN credentials cleared"})
|
||||
}
|
||||
|
||||
func handleSetWiFiAutoconnect(conn net.Conn, req Request, manager *Manager) {
|
||||
ssid, ok := req.Params["ssid"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
autoconnect, ok := req.Params["autoconnect"].(bool)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'autoconnect' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetWiFiAutoconnect(ssid, autoconnect); err != nil {
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to set autoconnect: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "autoconnect updated"})
|
||||
}
|
||||
263
core/internal/server/network/handlers_test.go
Normal file
263
core/internal/server/network/handlers_test.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockNetConn struct {
|
||||
net.Conn
|
||||
readBuf *bytes.Buffer
|
||||
writeBuf *bytes.Buffer
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newMockNetConn() *mockNetConn {
|
||||
return &mockNetConn{
|
||||
readBuf: &bytes.Buffer{},
|
||||
writeBuf: &bytes.Buffer{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockNetConn) Read(b []byte) (n int, err error) {
|
||||
return m.readBuf.Read(b)
|
||||
}
|
||||
|
||||
func (m *mockNetConn) Write(b []byte) (n int, err error) {
|
||||
return m.writeBuf.Write(b)
|
||||
}
|
||||
|
||||
func (m *mockNetConn) Close() error {
|
||||
m.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRespondError_Network(t *testing.T) {
|
||||
conn := newMockNetConn()
|
||||
models.RespondError(conn, 123, "test error")
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Equal(t, "test error", resp.Error)
|
||||
assert.Nil(t, resp.Result)
|
||||
}
|
||||
|
||||
func TestRespond_Network(t *testing.T) {
|
||||
conn := newMockNetConn()
|
||||
result := SuccessResult{Success: true, Message: "test"}
|
||||
models.Respond(conn, 123, result)
|
||||
|
||||
var resp models.Response[SuccessResult]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Success)
|
||||
assert.Equal(t, "test", resp.Result.Message)
|
||||
}
|
||||
|
||||
func TestHandleGetState(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
NetworkStatus: StatusWiFi,
|
||||
WiFiSSID: "TestNetwork",
|
||||
WiFiConnected: true,
|
||||
},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{ID: 123, Method: "network.getState"}
|
||||
|
||||
handleGetState(conn, req, manager)
|
||||
|
||||
var resp models.Response[NetworkState]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.Equal(t, StatusWiFi, resp.Result.NetworkStatus)
|
||||
assert.Equal(t, "TestNetwork", resp.Result.WiFiSSID)
|
||||
}
|
||||
|
||||
func TestHandleGetWiFiNetworks(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
WiFiNetworks: []WiFiNetwork{
|
||||
{SSID: "Network1", Signal: 90},
|
||||
{SSID: "Network2", Signal: 80},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{ID: 123, Method: "network.wifi.networks"}
|
||||
|
||||
handleGetWiFiNetworks(conn, req, manager)
|
||||
|
||||
var resp models.Response[[]WiFiNetwork]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.Len(t, *resp.Result, 2)
|
||||
assert.Equal(t, "Network1", (*resp.Result)[0].SSID)
|
||||
}
|
||||
|
||||
func TestHandleConnectWiFi(t *testing.T) {
|
||||
t.Run("missing ssid parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "network.wifi.connect",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleConnectWiFi(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'ssid' parameter")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleSetPreference(t *testing.T) {
|
||||
t.Run("missing preference parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "network.preference.set",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleSetPreference(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'preference' parameter")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleGetNetworkInfo(t *testing.T) {
|
||||
t.Run("missing ssid parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "network.info",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleGetNetworkInfo(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'ssid' parameter")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleRequest(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
NetworkStatus: StatusWiFi,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("unknown method", func(t *testing.T) {
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "network.unknown",
|
||||
}
|
||||
|
||||
HandleRequest(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "unknown method")
|
||||
})
|
||||
|
||||
t.Run("valid method - getState", func(t *testing.T) {
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "network.getState",
|
||||
}
|
||||
|
||||
HandleRequest(conn, req, manager)
|
||||
|
||||
var resp models.Response[NetworkState]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleSubscribe(t *testing.T) {
|
||||
// This test is complex due to the streaming nature of subscriptions
|
||||
// Better suited as an integration test
|
||||
t.Skip("Subscription test requires connection lifecycle management - integration test needed")
|
||||
}
|
||||
|
||||
func TestManager_Subscribe_Unsubscribe(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{},
|
||||
subscribers: make(map[string]chan NetworkState),
|
||||
}
|
||||
|
||||
t.Run("subscribe creates channel", func(t *testing.T) {
|
||||
ch := manager.Subscribe("client1")
|
||||
assert.NotNil(t, ch)
|
||||
assert.Len(t, manager.subscribers, 1)
|
||||
})
|
||||
|
||||
t.Run("unsubscribe removes channel", func(t *testing.T) {
|
||||
manager.Unsubscribe("client1")
|
||||
assert.Len(t, manager.subscribers, 0)
|
||||
})
|
||||
|
||||
t.Run("unsubscribe non-existent client is safe", func(t *testing.T) {
|
||||
assert.NotPanics(t, func() {
|
||||
manager.Unsubscribe("non-existent")
|
||||
})
|
||||
})
|
||||
}
|
||||
53
core/internal/server/network/helpers.go
Normal file
53
core/internal/server/network/helpers.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package network
|
||||
|
||||
import "sort"
|
||||
|
||||
func frequencyToChannel(freq uint32) uint32 {
|
||||
if freq >= 2412 && freq <= 2484 {
|
||||
if freq == 2484 {
|
||||
return 14
|
||||
}
|
||||
return (freq-2412)/5 + 1
|
||||
}
|
||||
|
||||
if freq >= 5170 && freq <= 5825 {
|
||||
return (freq-5170)/5 + 34
|
||||
}
|
||||
|
||||
if freq >= 5955 && freq <= 7115 {
|
||||
return (freq-5955)/5 + 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func sortWiFiNetworks(networks []WiFiNetwork) {
|
||||
sort.Slice(networks, func(i, j int) bool {
|
||||
if networks[i].Connected && !networks[j].Connected {
|
||||
return true
|
||||
}
|
||||
if !networks[i].Connected && networks[j].Connected {
|
||||
return false
|
||||
}
|
||||
|
||||
if networks[i].Saved && !networks[j].Saved {
|
||||
return true
|
||||
}
|
||||
if !networks[i].Saved && networks[j].Saved {
|
||||
return false
|
||||
}
|
||||
|
||||
if !networks[i].Secured && networks[j].Secured {
|
||||
if networks[i].Signal >= 50 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if networks[i].Secured && !networks[j].Secured {
|
||||
if networks[j].Signal >= 50 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return networks[i].Signal > networks[j].Signal
|
||||
})
|
||||
}
|
||||
530
core/internal/server/network/manager.go
Normal file
530
core/internal/server/network/manager.go
Normal file
@@ -0,0 +1,530 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
detection, err := DetectNetworkStack()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect network stack: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Network backend detection: %s", detection.ChosenReason)
|
||||
|
||||
var backend Backend
|
||||
switch detection.Backend {
|
||||
case BackendNetworkManager:
|
||||
nm, err := NewNetworkManagerBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create NetworkManager backend: %w", err)
|
||||
}
|
||||
backend = nm
|
||||
|
||||
case BackendIwd:
|
||||
iwd, err := NewIWDBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create iwd backend: %w", err)
|
||||
}
|
||||
backend = iwd
|
||||
|
||||
case BackendNetworkd:
|
||||
if detection.HasIwd && !detection.HasNM {
|
||||
wifi, err := NewIWDBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create iwd backend: %w", err)
|
||||
}
|
||||
l3, err := NewSystemdNetworkdBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create networkd backend: %w", err)
|
||||
}
|
||||
hybrid, err := NewHybridIwdNetworkdBackend(wifi, l3)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create hybrid backend: %w", err)
|
||||
}
|
||||
backend = hybrid
|
||||
} else {
|
||||
nd, err := NewSystemdNetworkdBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create networkd backend: %w", err)
|
||||
}
|
||||
backend = nd
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("no supported network backend found: %s", detection.ChosenReason)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
backend: backend,
|
||||
state: &NetworkState{
|
||||
NetworkStatus: StatusDisconnected,
|
||||
Preference: PreferenceAuto,
|
||||
WiFiNetworks: []WiFiNetwork{},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
subscribers: make(map[string]chan NetworkState),
|
||||
subMutex: sync.RWMutex{},
|
||||
stopChan: make(chan struct{}),
|
||||
dirty: make(chan struct{}, 1),
|
||||
credentialSubscribers: make(map[string]chan CredentialPrompt),
|
||||
credSubMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
broker := NewSubscriptionBroker(m.broadcastCredentialPrompt)
|
||||
if err := backend.SetPromptBroker(broker); err != nil {
|
||||
return nil, fmt.Errorf("failed to set prompt broker: %w", err)
|
||||
}
|
||||
|
||||
if err := backend.Initialize(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize backend: %w", err)
|
||||
}
|
||||
|
||||
if err := m.syncStateFromBackend(); err != nil {
|
||||
return nil, fmt.Errorf("failed to sync initial state: %w", err)
|
||||
}
|
||||
|
||||
m.notifierWg.Add(1)
|
||||
go m.notifier()
|
||||
|
||||
if err := backend.StartMonitoring(m.onBackendStateChange); err != nil {
|
||||
m.Close()
|
||||
return nil, fmt.Errorf("failed to start monitoring: %w", err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) syncStateFromBackend() error {
|
||||
backendState, err := m.backend.GetCurrentState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Backend = backendState.Backend
|
||||
m.state.NetworkStatus = backendState.NetworkStatus
|
||||
m.state.EthernetIP = backendState.EthernetIP
|
||||
m.state.EthernetDevice = backendState.EthernetDevice
|
||||
m.state.EthernetConnected = backendState.EthernetConnected
|
||||
m.state.EthernetConnectionUuid = backendState.EthernetConnectionUuid
|
||||
m.state.WiFiIP = backendState.WiFiIP
|
||||
m.state.WiFiDevice = backendState.WiFiDevice
|
||||
m.state.WiFiConnected = backendState.WiFiConnected
|
||||
m.state.WiFiEnabled = backendState.WiFiEnabled
|
||||
m.state.WiFiSSID = backendState.WiFiSSID
|
||||
m.state.WiFiBSSID = backendState.WiFiBSSID
|
||||
m.state.WiFiSignal = backendState.WiFiSignal
|
||||
m.state.WiFiNetworks = backendState.WiFiNetworks
|
||||
m.state.WiredConnections = backendState.WiredConnections
|
||||
m.state.VPNProfiles = backendState.VPNProfiles
|
||||
m.state.VPNActive = backendState.VPNActive
|
||||
m.state.IsConnecting = backendState.IsConnecting
|
||||
m.state.ConnectingSSID = backendState.ConnectingSSID
|
||||
m.state.LastError = backendState.LastError
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) onBackendStateChange() {
|
||||
if err := m.syncStateFromBackend(); err != nil {
|
||||
log.Errorf("failed to sync state from backend: %v", err)
|
||||
}
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
func signalChangeSignificant(old, new uint8) bool {
|
||||
if old == 0 || new == 0 {
|
||||
return true
|
||||
}
|
||||
diff := int(new) - int(old)
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
return diff >= 5
|
||||
}
|
||||
|
||||
func (m *Manager) snapshotState() NetworkState {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
s := *m.state
|
||||
s.WiFiNetworks = append([]WiFiNetwork(nil), m.state.WiFiNetworks...)
|
||||
s.WiredConnections = append([]WiredConnection(nil), m.state.WiredConnections...)
|
||||
s.VPNProfiles = append([]VPNProfile(nil), m.state.VPNProfiles...)
|
||||
s.VPNActive = append([]VPNActive(nil), m.state.VPNActive...)
|
||||
return s
|
||||
}
|
||||
|
||||
func stateChangedMeaningfully(old, new *NetworkState) bool {
|
||||
if old.NetworkStatus != new.NetworkStatus {
|
||||
return true
|
||||
}
|
||||
if old.Preference != new.Preference {
|
||||
return true
|
||||
}
|
||||
if old.EthernetConnected != new.EthernetConnected {
|
||||
return true
|
||||
}
|
||||
if old.EthernetIP != new.EthernetIP {
|
||||
return true
|
||||
}
|
||||
if old.WiFiConnected != new.WiFiConnected {
|
||||
return true
|
||||
}
|
||||
if old.WiFiEnabled != new.WiFiEnabled {
|
||||
return true
|
||||
}
|
||||
if old.WiFiSSID != new.WiFiSSID {
|
||||
return true
|
||||
}
|
||||
if old.WiFiBSSID != new.WiFiBSSID {
|
||||
return true
|
||||
}
|
||||
if old.WiFiIP != new.WiFiIP {
|
||||
return true
|
||||
}
|
||||
if !signalChangeSignificant(old.WiFiSignal, new.WiFiSignal) {
|
||||
if old.WiFiSignal != new.WiFiSignal {
|
||||
return false
|
||||
}
|
||||
} else if old.WiFiSignal != new.WiFiSignal {
|
||||
return true
|
||||
}
|
||||
if old.IsConnecting != new.IsConnecting {
|
||||
return true
|
||||
}
|
||||
if old.ConnectingSSID != new.ConnectingSSID {
|
||||
return true
|
||||
}
|
||||
if old.LastError != new.LastError {
|
||||
return true
|
||||
}
|
||||
if len(old.WiFiNetworks) != len(new.WiFiNetworks) {
|
||||
return true
|
||||
}
|
||||
if len(old.WiredConnections) != len(new.WiredConnections) {
|
||||
return true
|
||||
}
|
||||
|
||||
for i := range old.WiFiNetworks {
|
||||
oldNet := &old.WiFiNetworks[i]
|
||||
newNet := &new.WiFiNetworks[i]
|
||||
if oldNet.SSID != newNet.SSID {
|
||||
return true
|
||||
}
|
||||
if oldNet.Connected != newNet.Connected {
|
||||
return true
|
||||
}
|
||||
if oldNet.Saved != newNet.Saved {
|
||||
return true
|
||||
}
|
||||
if oldNet.Autoconnect != newNet.Autoconnect {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for i := range old.WiredConnections {
|
||||
oldNet := &old.WiredConnections[i]
|
||||
newNet := &new.WiredConnections[i]
|
||||
if oldNet.ID != newNet.ID {
|
||||
return true
|
||||
}
|
||||
if oldNet.IsActive != newNet.IsActive {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check VPN profiles count
|
||||
if len(old.VPNProfiles) != len(new.VPNProfiles) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check active VPN connections count or state
|
||||
if len(old.VPNActive) != len(new.VPNActive) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if any active VPN changed
|
||||
for i := range old.VPNActive {
|
||||
oldVPN := &old.VPNActive[i]
|
||||
newVPN := &new.VPNActive[i]
|
||||
if oldVPN.UUID != newVPN.UUID {
|
||||
return true
|
||||
}
|
||||
if oldVPN.State != newVPN.State {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() NetworkState {
|
||||
return m.snapshotState()
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan NetworkState {
|
||||
ch := make(chan NetworkState, 64)
|
||||
m.subMutex.Lock()
|
||||
m.subscribers[id] = ch
|
||||
m.subMutex.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
m.subMutex.Lock()
|
||||
if ch, ok := m.subscribers[id]; ok {
|
||||
close(ch)
|
||||
delete(m.subscribers, id)
|
||||
}
|
||||
m.subMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) SubscribeCredentials(id string) chan CredentialPrompt {
|
||||
ch := make(chan CredentialPrompt, 16)
|
||||
m.credSubMutex.Lock()
|
||||
m.credentialSubscribers[id] = ch
|
||||
m.credSubMutex.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) UnsubscribeCredentials(id string) {
|
||||
m.credSubMutex.Lock()
|
||||
if ch, ok := m.credentialSubscribers[id]; ok {
|
||||
close(ch)
|
||||
delete(m.credentialSubscribers, id)
|
||||
}
|
||||
m.credSubMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) broadcastCredentialPrompt(prompt CredentialPrompt) {
|
||||
m.credSubMutex.RLock()
|
||||
defer m.credSubMutex.RUnlock()
|
||||
|
||||
for _, ch := range m.credentialSubscribers {
|
||||
select {
|
||||
case ch <- prompt:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) notifier() {
|
||||
defer m.notifierWg.Done()
|
||||
const minGap = 100 * time.Millisecond
|
||||
timer := time.NewTimer(minGap)
|
||||
timer.Stop()
|
||||
var pending bool
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
timer.Stop()
|
||||
return
|
||||
case <-m.dirty:
|
||||
if pending {
|
||||
continue
|
||||
}
|
||||
pending = true
|
||||
timer.Reset(minGap)
|
||||
case <-timer.C:
|
||||
if !pending {
|
||||
continue
|
||||
}
|
||||
m.subMutex.RLock()
|
||||
if len(m.subscribers) == 0 {
|
||||
m.subMutex.RUnlock()
|
||||
pending = false
|
||||
continue
|
||||
}
|
||||
|
||||
currentState := m.snapshotState()
|
||||
|
||||
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, ¤tState) {
|
||||
m.subMutex.RUnlock()
|
||||
pending = false
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ch := range m.subscribers {
|
||||
select {
|
||||
case ch <- currentState:
|
||||
default:
|
||||
}
|
||||
}
|
||||
m.subMutex.RUnlock()
|
||||
|
||||
stateCopy := currentState
|
||||
m.lastNotifiedState = &stateCopy
|
||||
pending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) notifySubscribers() {
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SetPromptBroker(broker PromptBroker) error {
|
||||
return m.backend.SetPromptBroker(broker)
|
||||
}
|
||||
|
||||
func (m *Manager) SubmitCredentials(token string, secrets map[string]string, save bool) error {
|
||||
return m.backend.SubmitCredentials(token, secrets, save)
|
||||
}
|
||||
|
||||
func (m *Manager) CancelCredentials(token string) error {
|
||||
return m.backend.CancelCredentials(token)
|
||||
}
|
||||
|
||||
func (m *Manager) GetPromptBroker() PromptBroker {
|
||||
return m.backend.GetPromptBroker()
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
close(m.stopChan)
|
||||
m.notifierWg.Wait()
|
||||
|
||||
if m.backend != nil {
|
||||
m.backend.Close()
|
||||
}
|
||||
|
||||
m.subMutex.Lock()
|
||||
for _, ch := range m.subscribers {
|
||||
close(ch)
|
||||
}
|
||||
m.subscribers = make(map[string]chan NetworkState)
|
||||
m.subMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) ScanWiFi() error {
|
||||
return m.backend.ScanWiFi()
|
||||
}
|
||||
|
||||
func (m *Manager) GetWiFiNetworks() []WiFiNetwork {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
networks := make([]WiFiNetwork, len(m.state.WiFiNetworks))
|
||||
copy(networks, m.state.WiFiNetworks)
|
||||
return networks
|
||||
}
|
||||
|
||||
func (m *Manager) GetNetworkInfo(ssid string) (*WiFiNetwork, error) {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
|
||||
for _, network := range m.state.WiFiNetworks {
|
||||
if network.SSID == ssid {
|
||||
return &network, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("network not found: %s", ssid)
|
||||
}
|
||||
|
||||
func (m *Manager) GetNetworkInfoDetailed(ssid string) (*NetworkInfoResponse, error) {
|
||||
return m.backend.GetWiFiNetworkDetails(ssid)
|
||||
}
|
||||
|
||||
func (m *Manager) ToggleWiFi() error {
|
||||
enabled, err := m.backend.GetWiFiEnabled()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get WiFi state: %w", err)
|
||||
}
|
||||
|
||||
err = m.backend.SetWiFiEnabled(!enabled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to toggle WiFi: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) EnableWiFi() error {
|
||||
err := m.backend.SetWiFiEnabled(true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable WiFi: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) DisableWiFi() error {
|
||||
err := m.backend.SetWiFiEnabled(false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to disable WiFi: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) ConnectWiFi(req ConnectionRequest) error {
|
||||
return m.backend.ConnectWiFi(req)
|
||||
}
|
||||
|
||||
func (m *Manager) DisconnectWiFi() error {
|
||||
return m.backend.DisconnectWiFi()
|
||||
}
|
||||
|
||||
func (m *Manager) ForgetWiFiNetwork(ssid string) error {
|
||||
return m.backend.ForgetWiFiNetwork(ssid)
|
||||
}
|
||||
|
||||
func (m *Manager) GetWiredConfigs() []WiredConnection {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
configs := make([]WiredConnection, len(m.state.WiredConnections))
|
||||
copy(configs, m.state.WiredConnections)
|
||||
return configs
|
||||
}
|
||||
|
||||
func (m *Manager) GetWiredNetworkInfoDetailed(uuid string) (*WiredNetworkInfoResponse, error) {
|
||||
return m.backend.GetWiredNetworkDetails(uuid)
|
||||
}
|
||||
|
||||
func (m *Manager) ConnectEthernet() error {
|
||||
return m.backend.ConnectEthernet()
|
||||
}
|
||||
|
||||
func (m *Manager) DisconnectEthernet() error {
|
||||
return m.backend.DisconnectEthernet()
|
||||
}
|
||||
|
||||
func (m *Manager) activateConnection(uuid string) error {
|
||||
return m.backend.ActivateWiredConnection(uuid)
|
||||
}
|
||||
|
||||
func (m *Manager) ListVPNProfiles() ([]VPNProfile, error) {
|
||||
return m.backend.ListVPNProfiles()
|
||||
}
|
||||
|
||||
func (m *Manager) ListActiveVPN() ([]VPNActive, error) {
|
||||
return m.backend.ListActiveVPN()
|
||||
}
|
||||
|
||||
func (m *Manager) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||
return m.backend.ConnectVPN(uuidOrName, singleActive)
|
||||
}
|
||||
|
||||
func (m *Manager) DisconnectVPN(uuidOrName string) error {
|
||||
return m.backend.DisconnectVPN(uuidOrName)
|
||||
}
|
||||
|
||||
func (m *Manager) DisconnectAllVPN() error {
|
||||
return m.backend.DisconnectAllVPN()
|
||||
}
|
||||
|
||||
func (m *Manager) ClearVPNCredentials(uuidOrName string) error {
|
||||
return m.backend.ClearVPNCredentials(uuidOrName)
|
||||
}
|
||||
|
||||
func (m *Manager) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
|
||||
return m.backend.SetWiFiAutoconnect(ssid, autoconnect)
|
||||
}
|
||||
209
core/internal/server/network/manager_test.go
Normal file
209
core/internal/server/network/manager_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestManager_GetState(t *testing.T) {
|
||||
state := &NetworkState{
|
||||
NetworkStatus: StatusWiFi,
|
||||
WiFiSSID: "TestNetwork",
|
||||
WiFiConnected: true,
|
||||
}
|
||||
|
||||
manager := &Manager{
|
||||
state: state,
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
result := manager.GetState()
|
||||
assert.Equal(t, StatusWiFi, result.NetworkStatus)
|
||||
assert.Equal(t, "TestNetwork", result.WiFiSSID)
|
||||
assert.True(t, result.WiFiConnected)
|
||||
}
|
||||
|
||||
func TestManager_NotifySubscribers(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
NetworkStatus: StatusWiFi,
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
subscribers: make(map[string]chan NetworkState),
|
||||
subMutex: sync.RWMutex{},
|
||||
stopChan: make(chan struct{}),
|
||||
dirty: make(chan struct{}, 1),
|
||||
}
|
||||
manager.notifierWg.Add(1)
|
||||
go manager.notifier()
|
||||
|
||||
ch := make(chan NetworkState, 10)
|
||||
manager.subMutex.Lock()
|
||||
manager.subscribers["test-client"] = ch
|
||||
manager.subMutex.Unlock()
|
||||
|
||||
manager.notifySubscribers()
|
||||
|
||||
select {
|
||||
case state := <-ch:
|
||||
assert.Equal(t, StatusWiFi, state.NetworkStatus)
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatal("did not receive state update")
|
||||
}
|
||||
|
||||
close(manager.stopChan)
|
||||
manager.notifierWg.Wait()
|
||||
}
|
||||
|
||||
func TestManager_NotifySubscribers_Debounce(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
NetworkStatus: StatusWiFi,
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
subscribers: make(map[string]chan NetworkState),
|
||||
subMutex: sync.RWMutex{},
|
||||
stopChan: make(chan struct{}),
|
||||
dirty: make(chan struct{}, 1),
|
||||
}
|
||||
manager.notifierWg.Add(1)
|
||||
go manager.notifier()
|
||||
|
||||
ch := make(chan NetworkState, 10)
|
||||
manager.subMutex.Lock()
|
||||
manager.subscribers["test-client"] = ch
|
||||
manager.subMutex.Unlock()
|
||||
|
||||
manager.notifySubscribers()
|
||||
manager.notifySubscribers()
|
||||
manager.notifySubscribers()
|
||||
|
||||
receivedCount := 0
|
||||
timeout := time.After(200 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
receivedCount++
|
||||
case <-timeout:
|
||||
assert.Equal(t, 1, receivedCount, "should receive exactly one debounced update")
|
||||
close(manager.stopChan)
|
||||
manager.notifierWg.Wait()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_Close(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
subscribers: make(map[string]chan NetworkState),
|
||||
subMutex: sync.RWMutex{},
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
ch1 := make(chan NetworkState, 1)
|
||||
ch2 := make(chan NetworkState, 1)
|
||||
manager.subMutex.Lock()
|
||||
manager.subscribers["client1"] = ch1
|
||||
manager.subscribers["client2"] = ch2
|
||||
manager.subMutex.Unlock()
|
||||
|
||||
manager.Close()
|
||||
|
||||
select {
|
||||
case <-manager.stopChan:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("stopChan not closed")
|
||||
}
|
||||
|
||||
_, ok1 := <-ch1
|
||||
_, ok2 := <-ch2
|
||||
assert.False(t, ok1, "ch1 should be closed")
|
||||
assert.False(t, ok2, "ch2 should be closed")
|
||||
|
||||
assert.Len(t, manager.subscribers, 0)
|
||||
}
|
||||
|
||||
func TestManager_Subscribe(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{},
|
||||
subscribers: make(map[string]chan NetworkState),
|
||||
subMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
ch := manager.Subscribe("test-client")
|
||||
assert.NotNil(t, ch)
|
||||
assert.Equal(t, 64, cap(ch))
|
||||
|
||||
manager.subMutex.RLock()
|
||||
_, exists := manager.subscribers["test-client"]
|
||||
manager.subMutex.RUnlock()
|
||||
assert.True(t, exists)
|
||||
}
|
||||
|
||||
func TestManager_Unsubscribe(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{},
|
||||
subscribers: make(map[string]chan NetworkState),
|
||||
subMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
ch := manager.Subscribe("test-client")
|
||||
|
||||
manager.Unsubscribe("test-client")
|
||||
|
||||
_, ok := <-ch
|
||||
assert.False(t, ok)
|
||||
|
||||
manager.subMutex.RLock()
|
||||
_, exists := manager.subscribers["test-client"]
|
||||
manager.subMutex.RUnlock()
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
t.Run("attempts to create manager", func(t *testing.T) {
|
||||
manager, err := NewManager()
|
||||
if err != nil {
|
||||
assert.Nil(t, manager)
|
||||
} else {
|
||||
assert.NotNil(t, manager)
|
||||
assert.NotNil(t, manager.state)
|
||||
assert.NotNil(t, manager.subscribers)
|
||||
assert.NotNil(t, manager.stopChan)
|
||||
|
||||
manager.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_GetState_ThreadSafe(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
NetworkStatus: StatusWiFi,
|
||||
WiFiSSID: "TestNetwork",
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
state := manager.GetState()
|
||||
assert.Equal(t, StatusWiFi, state.NetworkStatus)
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timeout waiting for goroutines")
|
||||
}
|
||||
}
|
||||
}
|
||||
1
core/internal/server/network/monitor.go
Normal file
1
core/internal/server/network/monitor.go
Normal file
@@ -0,0 +1 @@
|
||||
package network
|
||||
138
core/internal/server/network/priority.go
Normal file
138
core/internal/server/network/priority.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
)
|
||||
|
||||
func (m *Manager) SetConnectionPreference(pref ConnectionPreference) error {
|
||||
switch pref {
|
||||
case PreferenceWiFi, PreferenceEthernet, PreferenceAuto:
|
||||
default:
|
||||
return fmt.Errorf("invalid preference: %s", pref)
|
||||
}
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Preference = pref
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
if _, ok := m.backend.(*NetworkManagerBackend); !ok {
|
||||
m.notifySubscribers()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch pref {
|
||||
case PreferenceWiFi:
|
||||
return m.prioritizeWiFi()
|
||||
case PreferenceEthernet:
|
||||
return m.prioritizeEthernet()
|
||||
case PreferenceAuto:
|
||||
return m.balancePriorities()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) prioritizeWiFi() error {
|
||||
if err := m.setConnectionMetrics("802-11-wireless", 50); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.setConnectionMetrics("802-3-ethernet", 100); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.notifySubscribers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) prioritizeEthernet() error {
|
||||
if err := m.setConnectionMetrics("802-3-ethernet", 50); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.setConnectionMetrics("802-11-wireless", 100); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.notifySubscribers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) balancePriorities() error {
|
||||
if err := m.setConnectionMetrics("802-3-ethernet", 50); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.setConnectionMetrics("802-11-wireless", 50); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.notifySubscribers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) setConnectionMetrics(connType string, metric uint32) error {
|
||||
settingsMgr, err := gonetworkmanager.NewSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
|
||||
connections, err := settingsMgr.ListConnections()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connections: %w", err)
|
||||
}
|
||||
|
||||
for _, conn := range connections {
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connMeta, ok := connSettings["connection"]; ok {
|
||||
if cType, ok := connMeta["type"].(string); ok && cType == connType {
|
||||
if connSettings["ipv4"] == nil {
|
||||
connSettings["ipv4"] = make(map[string]interface{})
|
||||
}
|
||||
if ipv4Map := connSettings["ipv4"]; ipv4Map != nil {
|
||||
ipv4Map["route-metric"] = int64(metric)
|
||||
}
|
||||
|
||||
if connSettings["ipv6"] == nil {
|
||||
connSettings["ipv6"] = make(map[string]interface{})
|
||||
}
|
||||
if ipv6Map := connSettings["ipv6"]; ipv6Map != nil {
|
||||
ipv6Map["route-metric"] = int64(metric)
|
||||
}
|
||||
|
||||
err = conn.Update(connSettings)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetConnectionPreference() ConnectionPreference {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
return m.state.Preference
|
||||
}
|
||||
|
||||
func (m *Manager) WasRecentlyFailed(ssid string) bool {
|
||||
if nm, ok := m.backend.(*NetworkManagerBackend); ok {
|
||||
nm.failedMutex.RLock()
|
||||
defer nm.failedMutex.RUnlock()
|
||||
|
||||
if nm.lastFailedSSID == ssid {
|
||||
elapsed := time.Now().Unix() - nm.lastFailedTime
|
||||
return elapsed < 10
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
50
core/internal/server/network/priority_test.go
Normal file
50
core/internal/server/network/priority_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestManager_SetConnectionPreference(t *testing.T) {
|
||||
t.Run("invalid preference", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
Preference: PreferenceAuto,
|
||||
},
|
||||
}
|
||||
|
||||
err := manager.SetConnectionPreference(ConnectionPreference("invalid"))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid preference")
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_GetConnectionPreference(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
preference ConnectionPreference
|
||||
}{
|
||||
{"auto", PreferenceAuto},
|
||||
{"wifi", PreferenceWiFi},
|
||||
{"ethernet", PreferenceEthernet},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
Preference: tt.preference,
|
||||
},
|
||||
}
|
||||
|
||||
result := manager.GetConnectionPreference()
|
||||
assert.Equal(t, tt.preference, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Full testing of priority operations would require mocking NetworkManager
|
||||
// D-Bus interfaces. The tests above cover the basic logic and error handling.
|
||||
// Integration tests would be needed for complete coverage of network connection
|
||||
// priority updates and reactivation.
|
||||
146
core/internal/server/network/subscription_broker.go
Normal file
146
core/internal/server/network/subscription_broker.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
type SubscriptionBroker struct {
|
||||
mu sync.RWMutex
|
||||
pending map[string]chan PromptReply
|
||||
requests map[string]PromptRequest
|
||||
pathSettingToToken map[string]string
|
||||
broadcastPrompt func(CredentialPrompt)
|
||||
}
|
||||
|
||||
func NewSubscriptionBroker(broadcastPrompt func(CredentialPrompt)) PromptBroker {
|
||||
return &SubscriptionBroker{
|
||||
pending: make(map[string]chan PromptReply),
|
||||
requests: make(map[string]PromptRequest),
|
||||
pathSettingToToken: make(map[string]string),
|
||||
broadcastPrompt: broadcastPrompt,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) {
|
||||
pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName)
|
||||
|
||||
b.mu.Lock()
|
||||
existingToken, alreadyPending := b.pathSettingToToken[pathSettingKey]
|
||||
b.mu.Unlock()
|
||||
|
||||
if alreadyPending {
|
||||
log.Infof("[SubscriptionBroker] Duplicate prompt for %s, returning existing token", pathSettingKey)
|
||||
return existingToken, nil
|
||||
}
|
||||
|
||||
token, err := generateToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
replyChan := make(chan PromptReply, 1)
|
||||
b.mu.Lock()
|
||||
b.pending[token] = replyChan
|
||||
b.requests[token] = req
|
||||
b.pathSettingToToken[pathSettingKey] = token
|
||||
b.mu.Unlock()
|
||||
|
||||
if b.broadcastPrompt != nil {
|
||||
prompt := CredentialPrompt{
|
||||
Token: token,
|
||||
Name: req.Name,
|
||||
SSID: req.SSID,
|
||||
ConnType: req.ConnType,
|
||||
VpnService: req.VpnService,
|
||||
Setting: req.SettingName,
|
||||
Fields: req.Fields,
|
||||
Hints: req.Hints,
|
||||
Reason: req.Reason,
|
||||
ConnectionId: req.ConnectionId,
|
||||
ConnectionUuid: req.ConnectionUuid,
|
||||
}
|
||||
b.broadcastPrompt(prompt)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
|
||||
b.mu.RLock()
|
||||
replyChan, exists := b.pending[token]
|
||||
b.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return PromptReply{}, fmt.Errorf("unknown token: %s", token)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
b.cleanup(token)
|
||||
return PromptReply{}, errdefs.ErrSecretPromptTimeout
|
||||
case reply := <-replyChan:
|
||||
b.cleanup(token)
|
||||
if reply.Cancel {
|
||||
return reply, errdefs.ErrSecretPromptCancelled
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
|
||||
b.mu.RLock()
|
||||
replyChan, exists := b.pending[token]
|
||||
b.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
log.Warnf("[SubscriptionBroker] Resolve: unknown or expired token: %s", token)
|
||||
return fmt.Errorf("unknown or expired token: %s", token)
|
||||
}
|
||||
|
||||
select {
|
||||
case replyChan <- reply:
|
||||
return nil
|
||||
default:
|
||||
log.Warnf("[SubscriptionBroker] Resolve: failed to deliver reply for token %s (channel full or closed)", token)
|
||||
return fmt.Errorf("failed to deliver reply for token: %s", token)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) cleanup(token string) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if req, exists := b.requests[token]; exists {
|
||||
pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName)
|
||||
delete(b.pathSettingToToken, pathSettingKey)
|
||||
}
|
||||
|
||||
delete(b.pending, token)
|
||||
delete(b.requests, token)
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) Cancel(path string, setting string) error {
|
||||
pathSettingKey := fmt.Sprintf("%s:%s", path, setting)
|
||||
|
||||
b.mu.Lock()
|
||||
token, exists := b.pathSettingToToken[pathSettingKey]
|
||||
b.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
log.Infof("[SubscriptionBroker] Cancel: no pending prompt for %s", pathSettingKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("[SubscriptionBroker] Cancelling prompt for %s (token=%s)", pathSettingKey, token)
|
||||
|
||||
reply := PromptReply{
|
||||
Cancel: true,
|
||||
}
|
||||
|
||||
return b.Resolve(token, reply)
|
||||
}
|
||||
15
core/internal/server/network/testing.go
Normal file
15
core/internal/server/network/testing.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package network
|
||||
|
||||
// NewTestManager creates a Manager for testing with a provided backend
|
||||
func NewTestManager(backend Backend, state *NetworkState) *Manager {
|
||||
if state == nil {
|
||||
state = &NetworkState{}
|
||||
}
|
||||
return &Manager{
|
||||
backend: backend,
|
||||
state: state,
|
||||
subscribers: make(map[string]chan NetworkState),
|
||||
stopChan: make(chan struct{}),
|
||||
dirty: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
190
core/internal/server/network/types.go
Normal file
190
core/internal/server/network/types.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type NetworkStatus string
|
||||
|
||||
const (
|
||||
StatusDisconnected NetworkStatus = "disconnected"
|
||||
StatusEthernet NetworkStatus = "ethernet"
|
||||
StatusWiFi NetworkStatus = "wifi"
|
||||
StatusVPN NetworkStatus = "vpn"
|
||||
)
|
||||
|
||||
type ConnectionPreference string
|
||||
|
||||
const (
|
||||
PreferenceAuto ConnectionPreference = "auto"
|
||||
PreferenceWiFi ConnectionPreference = "wifi"
|
||||
PreferenceEthernet ConnectionPreference = "ethernet"
|
||||
)
|
||||
|
||||
type WiFiNetwork struct {
|
||||
SSID string `json:"ssid"`
|
||||
BSSID string `json:"bssid"`
|
||||
Signal uint8 `json:"signal"`
|
||||
Secured bool `json:"secured"`
|
||||
Enterprise bool `json:"enterprise"`
|
||||
Connected bool `json:"connected"`
|
||||
Saved bool `json:"saved"`
|
||||
Autoconnect bool `json:"autoconnect"`
|
||||
Frequency uint32 `json:"frequency"`
|
||||
Mode string `json:"mode"`
|
||||
Rate uint32 `json:"rate"`
|
||||
Channel uint32 `json:"channel"`
|
||||
}
|
||||
|
||||
type VPNProfile struct {
|
||||
Name string `json:"name"`
|
||||
UUID string `json:"uuid"`
|
||||
Type string `json:"type"`
|
||||
ServiceType string `json:"serviceType"`
|
||||
}
|
||||
|
||||
type VPNActive struct {
|
||||
Name string `json:"name"`
|
||||
UUID string `json:"uuid"`
|
||||
Device string `json:"device,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Plugin string `json:"serviceType"`
|
||||
}
|
||||
|
||||
type VPNState struct {
|
||||
Profiles []VPNProfile `json:"profiles"`
|
||||
Active []VPNActive `json:"activeConnections"`
|
||||
}
|
||||
|
||||
type NetworkState struct {
|
||||
Backend string `json:"backend"`
|
||||
NetworkStatus NetworkStatus `json:"networkStatus"`
|
||||
Preference ConnectionPreference `json:"preference"`
|
||||
EthernetIP string `json:"ethernetIP"`
|
||||
EthernetDevice string `json:"ethernetDevice"`
|
||||
EthernetConnected bool `json:"ethernetConnected"`
|
||||
EthernetConnectionUuid string `json:"ethernetConnectionUuid"`
|
||||
WiFiIP string `json:"wifiIP"`
|
||||
WiFiDevice string `json:"wifiDevice"`
|
||||
WiFiConnected bool `json:"wifiConnected"`
|
||||
WiFiEnabled bool `json:"wifiEnabled"`
|
||||
WiFiSSID string `json:"wifiSSID"`
|
||||
WiFiBSSID string `json:"wifiBSSID"`
|
||||
WiFiSignal uint8 `json:"wifiSignal"`
|
||||
WiFiNetworks []WiFiNetwork `json:"wifiNetworks"`
|
||||
WiredConnections []WiredConnection `json:"wiredConnections"`
|
||||
VPNProfiles []VPNProfile `json:"vpnProfiles"`
|
||||
VPNActive []VPNActive `json:"vpnActive"`
|
||||
IsConnecting bool `json:"isConnecting"`
|
||||
ConnectingSSID string `json:"connectingSSID"`
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
type ConnectionRequest struct {
|
||||
SSID string `json:"ssid"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
|
||||
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
|
||||
Interactive bool `json:"interactive,omitempty"`
|
||||
}
|
||||
|
||||
type WiredConnection struct {
|
||||
Path dbus.ObjectPath `json:"path"`
|
||||
ID string `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Type string `json:"type"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
|
||||
type PriorityUpdate struct {
|
||||
Preference ConnectionPreference `json:"preference"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
backend Backend
|
||||
state *NetworkState
|
||||
stateMutex sync.RWMutex
|
||||
subscribers map[string]chan NetworkState
|
||||
subMutex sync.RWMutex
|
||||
stopChan chan struct{}
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotifiedState *NetworkState
|
||||
credentialSubscribers map[string]chan CredentialPrompt
|
||||
credSubMutex sync.RWMutex
|
||||
}
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventStateChanged EventType = "state_changed"
|
||||
EventNetworksUpdated EventType = "networks_updated"
|
||||
EventConnecting EventType = "connecting"
|
||||
EventConnected EventType = "connected"
|
||||
EventDisconnected EventType = "disconnected"
|
||||
EventError EventType = "error"
|
||||
)
|
||||
|
||||
type NetworkEvent struct {
|
||||
Type EventType `json:"type"`
|
||||
Data NetworkState `json:"data"`
|
||||
}
|
||||
|
||||
type PromptRequest struct {
|
||||
Name string `json:"name"`
|
||||
SSID string `json:"ssid"`
|
||||
ConnType string `json:"connType"`
|
||||
VpnService string `json:"vpnService"`
|
||||
SettingName string `json:"setting"`
|
||||
Fields []string `json:"fields"`
|
||||
Hints []string `json:"hints"`
|
||||
Reason string `json:"reason"`
|
||||
ConnectionId string `json:"connectionId"`
|
||||
ConnectionUuid string `json:"connectionUuid"`
|
||||
ConnectionPath string `json:"connectionPath"`
|
||||
}
|
||||
|
||||
type PromptReply struct {
|
||||
Secrets map[string]string `json:"secrets"`
|
||||
Save bool `json:"save"`
|
||||
Cancel bool `json:"cancel"`
|
||||
}
|
||||
|
||||
type CredentialPrompt struct {
|
||||
Token string `json:"token"`
|
||||
Name string `json:"name"`
|
||||
SSID string `json:"ssid"`
|
||||
ConnType string `json:"connType"`
|
||||
VpnService string `json:"vpnService"`
|
||||
Setting string `json:"setting"`
|
||||
Fields []string `json:"fields"`
|
||||
Hints []string `json:"hints"`
|
||||
Reason string `json:"reason"`
|
||||
ConnectionId string `json:"connectionId"`
|
||||
ConnectionUuid string `json:"connectionUuid"`
|
||||
}
|
||||
|
||||
type NetworkInfoResponse struct {
|
||||
SSID string `json:"ssid"`
|
||||
Bands []WiFiNetwork `json:"bands"`
|
||||
}
|
||||
|
||||
type WiredNetworkInfoResponse struct {
|
||||
UUID string `json:"uuid"`
|
||||
IFace string `json:"iface"`
|
||||
Driver string `json:"driver"`
|
||||
HwAddr string `json:"hwAddr"`
|
||||
Speed string `json:"speed"`
|
||||
IPv4 WiredIPConfig `json:"IPv4s"`
|
||||
IPv6 WiredIPConfig `json:"IPv6s"`
|
||||
}
|
||||
|
||||
type WiredIPConfig struct {
|
||||
IPs []string `json:"ips"`
|
||||
Gateway string `json:"gateway"`
|
||||
DNS string `json:"dns"`
|
||||
}
|
||||
178
core/internal/server/network/types_test.go
Normal file
178
core/internal/server/network/types_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNetworkStatus_Constants(t *testing.T) {
|
||||
assert.Equal(t, NetworkStatus("disconnected"), StatusDisconnected)
|
||||
assert.Equal(t, NetworkStatus("ethernet"), StatusEthernet)
|
||||
assert.Equal(t, NetworkStatus("wifi"), StatusWiFi)
|
||||
}
|
||||
|
||||
func TestConnectionPreference_Constants(t *testing.T) {
|
||||
assert.Equal(t, ConnectionPreference("auto"), PreferenceAuto)
|
||||
assert.Equal(t, ConnectionPreference("wifi"), PreferenceWiFi)
|
||||
assert.Equal(t, ConnectionPreference("ethernet"), PreferenceEthernet)
|
||||
}
|
||||
|
||||
func TestEventType_Constants(t *testing.T) {
|
||||
assert.Equal(t, EventType("state_changed"), EventStateChanged)
|
||||
assert.Equal(t, EventType("networks_updated"), EventNetworksUpdated)
|
||||
assert.Equal(t, EventType("connecting"), EventConnecting)
|
||||
assert.Equal(t, EventType("connected"), EventConnected)
|
||||
assert.Equal(t, EventType("disconnected"), EventDisconnected)
|
||||
assert.Equal(t, EventType("error"), EventError)
|
||||
}
|
||||
|
||||
func TestWiFiNetwork_JSON(t *testing.T) {
|
||||
network := WiFiNetwork{
|
||||
SSID: "TestNetwork",
|
||||
BSSID: "00:11:22:33:44:55",
|
||||
Signal: 85,
|
||||
Secured: true,
|
||||
Enterprise: false,
|
||||
Connected: true,
|
||||
Saved: true,
|
||||
Frequency: 2437,
|
||||
Mode: "infrastructure",
|
||||
Rate: 300,
|
||||
Channel: 6,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(network)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded WiFiNetwork
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, network.SSID, decoded.SSID)
|
||||
assert.Equal(t, network.BSSID, decoded.BSSID)
|
||||
assert.Equal(t, network.Signal, decoded.Signal)
|
||||
assert.Equal(t, network.Secured, decoded.Secured)
|
||||
assert.Equal(t, network.Enterprise, decoded.Enterprise)
|
||||
assert.Equal(t, network.Connected, decoded.Connected)
|
||||
assert.Equal(t, network.Saved, decoded.Saved)
|
||||
assert.Equal(t, network.Frequency, decoded.Frequency)
|
||||
assert.Equal(t, network.Mode, decoded.Mode)
|
||||
assert.Equal(t, network.Rate, decoded.Rate)
|
||||
assert.Equal(t, network.Channel, decoded.Channel)
|
||||
}
|
||||
|
||||
func TestNetworkState_JSON(t *testing.T) {
|
||||
state := NetworkState{
|
||||
NetworkStatus: StatusWiFi,
|
||||
Preference: PreferenceAuto,
|
||||
EthernetIP: "192.168.1.100",
|
||||
EthernetDevice: "eth0",
|
||||
EthernetConnected: false,
|
||||
WiFiIP: "192.168.1.101",
|
||||
WiFiDevice: "wlan0",
|
||||
WiFiConnected: true,
|
||||
WiFiEnabled: true,
|
||||
WiFiSSID: "TestNetwork",
|
||||
WiFiBSSID: "00:11:22:33:44:55",
|
||||
WiFiSignal: 85,
|
||||
WiFiNetworks: []WiFiNetwork{
|
||||
{SSID: "Network1", Signal: 90},
|
||||
{SSID: "Network2", Signal: 60},
|
||||
},
|
||||
IsConnecting: false,
|
||||
ConnectingSSID: "",
|
||||
LastError: "",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(state)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded NetworkState
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, state.NetworkStatus, decoded.NetworkStatus)
|
||||
assert.Equal(t, state.Preference, decoded.Preference)
|
||||
assert.Equal(t, state.WiFiIP, decoded.WiFiIP)
|
||||
assert.Equal(t, state.WiFiSSID, decoded.WiFiSSID)
|
||||
assert.Equal(t, len(state.WiFiNetworks), len(decoded.WiFiNetworks))
|
||||
}
|
||||
|
||||
func TestConnectionRequest_JSON(t *testing.T) {
|
||||
t.Run("with password", func(t *testing.T) {
|
||||
req := ConnectionRequest{
|
||||
SSID: "TestNetwork",
|
||||
Password: "testpass123",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded ConnectionRequest
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, req.SSID, decoded.SSID)
|
||||
assert.Equal(t, req.Password, decoded.Password)
|
||||
assert.Empty(t, decoded.Username)
|
||||
})
|
||||
|
||||
t.Run("with username and password (enterprise)", func(t *testing.T) {
|
||||
req := ConnectionRequest{
|
||||
SSID: "EnterpriseNetwork",
|
||||
Password: "testpass123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded ConnectionRequest
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, req.SSID, decoded.SSID)
|
||||
assert.Equal(t, req.Password, decoded.Password)
|
||||
assert.Equal(t, req.Username, decoded.Username)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPriorityUpdate_JSON(t *testing.T) {
|
||||
update := PriorityUpdate{
|
||||
Preference: PreferenceWiFi,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(update)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded PriorityUpdate
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, update.Preference, decoded.Preference)
|
||||
}
|
||||
|
||||
func TestNetworkEvent_JSON(t *testing.T) {
|
||||
event := NetworkEvent{
|
||||
Type: EventStateChanged,
|
||||
Data: NetworkState{
|
||||
NetworkStatus: StatusWiFi,
|
||||
WiFiSSID: "TestNetwork",
|
||||
WiFiConnected: true,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(event)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded NetworkEvent
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, event.Type, decoded.Type)
|
||||
assert.Equal(t, event.Data.NetworkStatus, decoded.Data.NetworkStatus)
|
||||
assert.Equal(t, event.Data.WiFiSSID, decoded.Data.WiFiSSID)
|
||||
}
|
||||
148
core/internal/server/network/wifi_test.go
Normal file
148
core/internal/server/network/wifi_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFrequencyToChannel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
frequency uint32
|
||||
channel uint32
|
||||
}{
|
||||
{"2.4 GHz channel 1", 2412, 1},
|
||||
{"2.4 GHz channel 6", 2437, 6},
|
||||
{"2.4 GHz channel 11", 2462, 11},
|
||||
{"2.4 GHz channel 14", 2484, 14},
|
||||
{"5 GHz channel 36", 5180, 36},
|
||||
{"5 GHz channel 40", 5200, 40},
|
||||
{"5 GHz channel 165", 5825, 165},
|
||||
{"6 GHz channel 1", 5955, 1},
|
||||
{"6 GHz channel 233", 7115, 233},
|
||||
{"Unknown frequency", 1000, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := frequencyToChannel(tt.frequency)
|
||||
assert.Equal(t, tt.channel, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortWiFiNetworks(t *testing.T) {
|
||||
t.Run("connected network comes first", func(t *testing.T) {
|
||||
networks := []WiFiNetwork{
|
||||
{SSID: "Network1", Signal: 90, Connected: false},
|
||||
{SSID: "Network2", Signal: 80, Connected: true},
|
||||
{SSID: "Network3", Signal: 70, Connected: false},
|
||||
}
|
||||
|
||||
sortWiFiNetworks(networks)
|
||||
|
||||
assert.Equal(t, "Network2", networks[0].SSID)
|
||||
assert.True(t, networks[0].Connected)
|
||||
})
|
||||
|
||||
t.Run("sorts by signal strength", func(t *testing.T) {
|
||||
networks := []WiFiNetwork{
|
||||
{SSID: "Weak", Signal: 40, Secured: true},
|
||||
{SSID: "Strong", Signal: 90, Secured: true},
|
||||
{SSID: "Medium", Signal: 60, Secured: true},
|
||||
}
|
||||
|
||||
sortWiFiNetworks(networks)
|
||||
|
||||
assert.Equal(t, "Strong", networks[0].SSID)
|
||||
assert.Equal(t, "Medium", networks[1].SSID)
|
||||
assert.Equal(t, "Weak", networks[2].SSID)
|
||||
})
|
||||
|
||||
t.Run("prioritizes open networks with good signal", func(t *testing.T) {
|
||||
networks := []WiFiNetwork{
|
||||
{SSID: "SecureWeak", Signal: 40, Secured: true},
|
||||
{SSID: "OpenStrong", Signal: 60, Secured: false},
|
||||
{SSID: "SecureStrong", Signal: 90, Secured: true},
|
||||
}
|
||||
|
||||
sortWiFiNetworks(networks)
|
||||
|
||||
assert.Equal(t, "OpenStrong", networks[0].SSID)
|
||||
|
||||
openIdx := -1
|
||||
weakSecureIdx := -1
|
||||
for i, n := range networks {
|
||||
if n.SSID == "OpenStrong" {
|
||||
openIdx = i
|
||||
}
|
||||
if n.SSID == "SecureWeak" {
|
||||
weakSecureIdx = i
|
||||
}
|
||||
}
|
||||
assert.Less(t, openIdx, weakSecureIdx, "OpenStrong should come before SecureWeak")
|
||||
})
|
||||
|
||||
t.Run("prioritizes saved networks after connected", func(t *testing.T) {
|
||||
networks := []WiFiNetwork{
|
||||
{SSID: "UnsavedStrong", Signal: 95, Saved: false},
|
||||
{SSID: "SavedMedium", Signal: 60, Saved: true},
|
||||
{SSID: "SavedWeak", Signal: 50, Saved: true},
|
||||
{SSID: "UnsavedMedium", Signal: 70, Saved: false},
|
||||
}
|
||||
|
||||
sortWiFiNetworks(networks)
|
||||
|
||||
assert.Equal(t, "SavedMedium", networks[0].SSID)
|
||||
assert.Equal(t, "SavedWeak", networks[1].SSID)
|
||||
assert.Equal(t, "UnsavedStrong", networks[2].SSID)
|
||||
assert.Equal(t, "UnsavedMedium", networks[3].SSID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_GetWiFiNetworks(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
WiFiNetworks: []WiFiNetwork{
|
||||
{SSID: "Network1", Signal: 90},
|
||||
{SSID: "Network2", Signal: 80},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
networks := manager.GetWiFiNetworks()
|
||||
|
||||
assert.Len(t, networks, 2)
|
||||
assert.Equal(t, "Network1", networks[0].SSID)
|
||||
assert.Equal(t, "Network2", networks[1].SSID)
|
||||
|
||||
networks[0].SSID = "Modified"
|
||||
assert.Equal(t, "Network1", manager.state.WiFiNetworks[0].SSID)
|
||||
}
|
||||
|
||||
func TestManager_GetNetworkInfo(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
WiFiNetworks: []WiFiNetwork{
|
||||
{SSID: "Network1", Signal: 90, BSSID: "00:11:22:33:44:55"},
|
||||
{SSID: "Network2", Signal: 80, BSSID: "AA:BB:CC:DD:EE:FF"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("finds existing network", func(t *testing.T) {
|
||||
network, err := manager.GetNetworkInfo("Network1")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, network)
|
||||
assert.Equal(t, "Network1", network.SSID)
|
||||
assert.Equal(t, uint8(90), network.Signal)
|
||||
})
|
||||
|
||||
t.Run("returns error for non-existent network", func(t *testing.T) {
|
||||
network, err := manager.GetNetworkInfo("NonExistent")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, network)
|
||||
assert.Contains(t, err.Error(), "network not found")
|
||||
})
|
||||
}
|
||||
23
core/internal/server/network/wired_test.go
Normal file
23
core/internal/server/network/wired_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestManager_GetWiredConfigs(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &NetworkState{
|
||||
EthernetConnected: true,
|
||||
WiredConnections: []WiredConnection{
|
||||
{ID: "Test", IsActive: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
configs := manager.GetWiredConfigs()
|
||||
|
||||
assert.Len(t, configs, 1)
|
||||
assert.Equal(t, "Test", configs[0].ID)
|
||||
}
|
||||
Reference in New Issue
Block a user