mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-14 03:43:43 +08:00
100 lines
3.5 KiB
Go
100 lines
3.5 KiB
Go
//go:build linux
|
|
|
|
package device
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"vocat/internal/modem"
|
|
)
|
|
|
|
func setQMINetwork(
|
|
ctx context.Context,
|
|
candidate modem.Candidate,
|
|
enabled bool,
|
|
apn string,
|
|
ipVersion string,
|
|
) (NetworkResult, error) {
|
|
qmiNetwork, err := exec.LookPath("qmi-network")
|
|
if err != nil {
|
|
return NetworkResult{}, fmt.Errorf("%w: install libqmi-utils to control %s", ErrDataBackendUnavailable, candidate.QMIControl)
|
|
}
|
|
profile, err := os.CreateTemp("", "vocat-qmi-*.conf")
|
|
if err != nil {
|
|
return NetworkResult{}, fmt.Errorf("create temporary QMI profile: %w", err)
|
|
}
|
|
profilePath := profile.Name()
|
|
defer os.Remove(profilePath)
|
|
ipType := map[string]string{"IP": "4", "IPV6": "6", "IPV4V6": "4"}[ipVersion]
|
|
if _, err := fmt.Fprintf(profile, "APN=%s\nIP_TYPE=%s\nPROXY=yes\n", apn, ipType); err != nil {
|
|
_ = profile.Close()
|
|
return NetworkResult{}, fmt.Errorf("write temporary QMI profile: %w", err)
|
|
}
|
|
if err := profile.Chmod(0o600); err != nil {
|
|
_ = profile.Close()
|
|
return NetworkResult{}, fmt.Errorf("protect temporary QMI profile: %w", err)
|
|
}
|
|
if err := profile.Close(); err != nil {
|
|
return NetworkResult{}, fmt.Errorf("close temporary QMI profile: %w", err)
|
|
}
|
|
|
|
action := "stop"
|
|
if enabled {
|
|
action = "start"
|
|
}
|
|
command := exec.CommandContext(ctx, qmiNetwork, "--profile="+profilePath, candidate.QMIControl, action)
|
|
output, err := command.CombinedOutput()
|
|
detail := strings.TrimSpace(string(output))
|
|
if err != nil {
|
|
lowerDetail := strings.ToLower(detail)
|
|
idempotentStop := !enabled && (strings.Contains(lowerDetail, "already stopped") ||
|
|
strings.Contains(lowerDetail, "not started") || strings.Contains(lowerDetail, "no network"))
|
|
if !idempotentStop {
|
|
return NetworkResult{}, fmt.Errorf("qmi-network %s failed: %w: %s", action, err, detail)
|
|
}
|
|
}
|
|
if ipCommand, lookErr := exec.LookPath("ip"); lookErr == nil {
|
|
linkAction := "down"
|
|
if enabled {
|
|
linkAction = "up"
|
|
}
|
|
linkOutput, linkErr := exec.CommandContext(ctx, ipCommand, "link", "set", "dev", candidate.NetworkInterface, linkAction).CombinedOutput()
|
|
if linkErr != nil {
|
|
return NetworkResult{}, fmt.Errorf("set %s %s: %w: %s", candidate.NetworkInterface, linkAction, linkErr, strings.TrimSpace(string(linkOutput)))
|
|
}
|
|
}
|
|
if enabled {
|
|
if busybox, lookErr := exec.LookPath("busybox"); lookErr == nil {
|
|
dhcpOutput, dhcpErr := exec.CommandContext(ctx, busybox, "udhcpc", "-q", "-n", "-t", "5", "-T", "3", "-i", candidate.NetworkInterface).CombinedOutput()
|
|
if dhcpErr != nil {
|
|
rollbackCtx, cancelRollback := context.WithTimeout(context.Background(), managerCommandCleanupTimeout)
|
|
defer cancelRollback()
|
|
_, _ = exec.CommandContext(rollbackCtx, qmiNetwork, "--profile="+profilePath, candidate.QMIControl, "stop").CombinedOutput()
|
|
if ipCommand, lookErr := exec.LookPath("ip"); lookErr == nil {
|
|
_, _ = exec.CommandContext(rollbackCtx, ipCommand, "link", "set", "dev", candidate.NetworkInterface, "down").CombinedOutput()
|
|
}
|
|
return NetworkResult{}, fmt.Errorf("QMI session started but DHCP failed: %w: %s", dhcpErr, strings.TrimSpace(string(dhcpOutput)))
|
|
}
|
|
if value := strings.TrimSpace(string(dhcpOutput)); value != "" {
|
|
detail = strings.TrimSpace(detail + "\n" + value)
|
|
}
|
|
}
|
|
}
|
|
return NetworkResult{
|
|
Enabled: enabled,
|
|
Backend: "qmi",
|
|
Interface: candidate.NetworkInterface,
|
|
ControlDevice: candidate.QMIControl,
|
|
APN: apn,
|
|
IPVersion: ipVersion,
|
|
Detail: detail,
|
|
}, nil
|
|
}
|
|
|
|
const managerCommandCleanupTimeout = 15 * time.Second
|