data_linux.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. //go:build linux
  2. package device
  3. import (
  4. "context"
  5. "fmt"
  6. "os"
  7. "os/exec"
  8. "strings"
  9. "time"
  10. "vocat/internal/modem"
  11. )
  12. func setQMINetwork(
  13. ctx context.Context,
  14. candidate modem.Candidate,
  15. enabled bool,
  16. apn string,
  17. ipVersion string,
  18. ) (NetworkResult, error) {
  19. qmiNetwork, err := exec.LookPath("qmi-network")
  20. if err != nil {
  21. return NetworkResult{}, fmt.Errorf("%w: install libqmi-utils to control %s", ErrDataBackendUnavailable, candidate.QMIControl)
  22. }
  23. profile, err := os.CreateTemp("", "vocat-qmi-*.conf")
  24. if err != nil {
  25. return NetworkResult{}, fmt.Errorf("create temporary QMI profile: %w", err)
  26. }
  27. profilePath := profile.Name()
  28. defer os.Remove(profilePath)
  29. ipType := map[string]string{"IP": "4", "IPV6": "6", "IPV4V6": "4"}[ipVersion]
  30. if _, err := fmt.Fprintf(profile, "APN=%s\nIP_TYPE=%s\nPROXY=yes\n", apn, ipType); err != nil {
  31. _ = profile.Close()
  32. return NetworkResult{}, fmt.Errorf("write temporary QMI profile: %w", err)
  33. }
  34. if err := profile.Chmod(0o600); err != nil {
  35. _ = profile.Close()
  36. return NetworkResult{}, fmt.Errorf("protect temporary QMI profile: %w", err)
  37. }
  38. if err := profile.Close(); err != nil {
  39. return NetworkResult{}, fmt.Errorf("close temporary QMI profile: %w", err)
  40. }
  41. action := "stop"
  42. if enabled {
  43. action = "start"
  44. }
  45. command := exec.CommandContext(ctx, qmiNetwork, "--profile="+profilePath, candidate.QMIControl, action)
  46. output, err := command.CombinedOutput()
  47. detail := strings.TrimSpace(string(output))
  48. if err != nil {
  49. lowerDetail := strings.ToLower(detail)
  50. idempotentStop := !enabled && (strings.Contains(lowerDetail, "already stopped") ||
  51. strings.Contains(lowerDetail, "not started") || strings.Contains(lowerDetail, "no network"))
  52. if !idempotentStop {
  53. return NetworkResult{}, fmt.Errorf("qmi-network %s failed: %w: %s", action, err, detail)
  54. }
  55. }
  56. if ipCommand, lookErr := exec.LookPath("ip"); lookErr == nil {
  57. linkAction := "down"
  58. if enabled {
  59. linkAction = "up"
  60. }
  61. linkOutput, linkErr := exec.CommandContext(ctx, ipCommand, "link", "set", "dev", candidate.NetworkInterface, linkAction).CombinedOutput()
  62. if linkErr != nil {
  63. return NetworkResult{}, fmt.Errorf("set %s %s: %w: %s", candidate.NetworkInterface, linkAction, linkErr, strings.TrimSpace(string(linkOutput)))
  64. }
  65. }
  66. if enabled {
  67. if busybox, lookErr := exec.LookPath("busybox"); lookErr == nil {
  68. dhcpOutput, dhcpErr := exec.CommandContext(ctx, busybox, "udhcpc", "-q", "-n", "-t", "5", "-T", "3", "-i", candidate.NetworkInterface).CombinedOutput()
  69. if dhcpErr != nil {
  70. rollbackCtx, cancelRollback := context.WithTimeout(context.Background(), managerCommandCleanupTimeout)
  71. defer cancelRollback()
  72. _, _ = exec.CommandContext(rollbackCtx, qmiNetwork, "--profile="+profilePath, candidate.QMIControl, "stop").CombinedOutput()
  73. if ipCommand, lookErr := exec.LookPath("ip"); lookErr == nil {
  74. _, _ = exec.CommandContext(rollbackCtx, ipCommand, "link", "set", "dev", candidate.NetworkInterface, "down").CombinedOutput()
  75. }
  76. return NetworkResult{}, fmt.Errorf("QMI session started but DHCP failed: %w: %s", dhcpErr, strings.TrimSpace(string(dhcpOutput)))
  77. }
  78. if value := strings.TrimSpace(string(dhcpOutput)); value != "" {
  79. detail = strings.TrimSpace(detail + "\n" + value)
  80. }
  81. }
  82. }
  83. return NetworkResult{
  84. Enabled: enabled,
  85. Backend: "qmi",
  86. Interface: candidate.NetworkInterface,
  87. ControlDevice: candidate.QMIControl,
  88. APN: apn,
  89. IPVersion: ipVersion,
  90. Detail: detail,
  91. }, nil
  92. }
  93. const managerCommandCleanupTimeout = 15 * time.Second