| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- //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
|