store.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. // Package integration connects the protocol runtime to vocat's persistent
  2. // configuration and modem inventory. It contains no IKE, IMS, or SIM protocol
  3. // implementation.
  4. package integration
  5. import (
  6. "context"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "strings"
  11. "vocat/internal/device"
  12. "vocat/internal/store"
  13. "vocat/internal/vowifi"
  14. )
  15. type ProxyResolver struct {
  16. Store *store.Store
  17. }
  18. func (resolver ProxyResolver) Resolve(
  19. ctx context.Context,
  20. request vowifi.ProxyRequest,
  21. ) (vowifi.ProxyRoute, error) {
  22. if resolver.Store == nil {
  23. return vowifi.ProxyRoute{}, errors.New("vowifi proxy resolver: store is nil")
  24. }
  25. deviceID := strings.TrimSpace(request.DeviceID)
  26. if deviceID == "" {
  27. return vowifi.ProxyRoute{Mode: vowifi.ProxyModeDirect}, nil
  28. }
  29. binding, err := resolver.Store.DeviceProxyBinding(ctx, deviceID)
  30. if errors.Is(err, store.ErrNotFound) {
  31. return vowifi.ProxyRoute{Mode: vowifi.ProxyModeDirect}, nil
  32. }
  33. if err != nil {
  34. return vowifi.ProxyRoute{}, fmt.Errorf("resolve proxy binding for device %s: %w", deviceID, err)
  35. }
  36. upstream, err := resolver.Store.UpstreamProxy(ctx, binding.UpstreamProxyID)
  37. if err != nil {
  38. return vowifi.ProxyRoute{}, fmt.Errorf(
  39. "load upstream proxy %q for device %s: %w",
  40. binding.UpstreamProxyID,
  41. deviceID,
  42. err,
  43. )
  44. }
  45. if !upstream.Enabled {
  46. return vowifi.ProxyRoute{}, fmt.Errorf(
  47. "upstream proxy %q for device %s is disabled",
  48. upstream.ID,
  49. deviceID,
  50. )
  51. }
  52. return vowifi.ProxyRoute{
  53. Mode: vowifi.ProxyModeSOCKS5,
  54. ID: upstream.ID,
  55. Address: upstream.Addr,
  56. Username: upstream.Username,
  57. Password: upstream.Password,
  58. }, nil
  59. }
  60. type PhoneStore struct {
  61. Store *store.Store
  62. DeviceID string
  63. }
  64. func (phones PhoneStore) SaveAssociatedNumber(
  65. ctx context.Context,
  66. record vowifi.PhoneRecord,
  67. ) error {
  68. if phones.Store == nil {
  69. return errors.New("vowifi phone store: store is nil")
  70. }
  71. switch record.Source {
  72. case vowifi.PhoneSourceAssociatedMSISDN, vowifi.PhoneSourcePAssociatedURI:
  73. default:
  74. return fmt.Errorf("vowifi phone store: untrusted source %q", record.Source)
  75. }
  76. return phones.Store.UpsertPhoneAssociation(ctx, store.PhoneAssociation{
  77. ICCID: record.ICCID,
  78. DeviceID: strings.TrimSpace(phones.DeviceID),
  79. Number: record.Number,
  80. Source: record.Source,
  81. UpdatedAt: record.UpdatedAt,
  82. })
  83. }
  84. type DeviceReader interface {
  85. Get(string) (device.Device, error)
  86. }
  87. type StateProjector struct {
  88. Store *store.Store
  89. Devices DeviceReader
  90. }
  91. func (projector StateProjector) Save(
  92. ctx context.Context,
  93. state vowifi.State,
  94. ) error {
  95. if projector.Store == nil {
  96. return errors.New("vowifi state projector: store is nil")
  97. }
  98. runtime := store.VoWiFiRuntime{
  99. DeviceID: state.DeviceID,
  100. Phase: string(state.Phase),
  101. DataplaneMode: dataplaneMode(state),
  102. SIMReady: state.SIMReady,
  103. AccessReady: state.AccessReady,
  104. TunnelReady: state.TunnelReady,
  105. IMSReady: state.IMSReady,
  106. SMSReady: state.SMSReady,
  107. RegStatus: boolInt(state.IMSReady),
  108. RegStatusText: registrationText(state),
  109. NetworkMode: "Wi-Fi",
  110. LocalPhone: state.PhoneNumber,
  111. PhoneNumberSource: state.PhoneNumberSource,
  112. LastErrorClass: state.LastErrorClass,
  113. LastError: state.LastError,
  114. LastReason: state.LastReason,
  115. UpdatedAt: state.UpdatedAt,
  116. }
  117. if projector.Devices != nil {
  118. if entry, err := projector.Devices.Get(state.DeviceID); err == nil && entry.Snapshot != nil {
  119. runtime.ICCID = strings.TrimSpace(entry.Snapshot.ICCID)
  120. runtime.IMSI = strings.TrimSpace(entry.Snapshot.IMSI)
  121. }
  122. }
  123. if runtime.LocalPhone == "" && runtime.ICCID != "" {
  124. if association, err := projector.Store.PhoneAssociation(ctx, runtime.ICCID); err == nil {
  125. runtime.LocalPhone = association.Number
  126. runtime.PhoneNumberSource = association.Source
  127. } else if !errors.Is(err, store.ErrNotFound) {
  128. return fmt.Errorf("restore associated phone number: %w", err)
  129. }
  130. }
  131. var err error
  132. runtime.Tunnel, err = marshalObject(map[string]any{
  133. "established": state.TunnelReady,
  134. "name": state.TunnelName,
  135. "dataplane_mode": state.DataplaneMode,
  136. "epdg": state.EPDG,
  137. "proxy_mode": state.ProxyMode,
  138. "proxy_id": state.ProxyID,
  139. "security_audit": state.Security,
  140. })
  141. if err != nil {
  142. return err
  143. }
  144. runtime.IMSCore, err = marshalObject(map[string]any{
  145. "registered": state.IMSReady,
  146. "registration_state": state.IMSRegistration,
  147. "associated_number": runtime.LocalPhone,
  148. "number_source": runtime.PhoneNumberSource,
  149. })
  150. if err != nil {
  151. return err
  152. }
  153. runtime.SMSIP, err = marshalObject(map[string]any{
  154. "ready": state.SMSReady,
  155. })
  156. if err != nil {
  157. return err
  158. }
  159. runtime.Extra, err = marshalObject(map[string]any{
  160. "enabled": state.Enabled,
  161. "active": state.Active,
  162. "pure_airplane_policy": state.PureAirplanePolicy,
  163. "home_mcc": state.HomeMCC,
  164. "home_mnc": state.HomeMNC,
  165. "warnings": state.Warnings,
  166. "cleanup_errors": state.CleanupErrors,
  167. "attempt": state.Attempt,
  168. "sequence": state.Sequence,
  169. "started_at": state.StartedAt,
  170. })
  171. if err != nil {
  172. return err
  173. }
  174. return projector.Store.UpsertVoWiFiRuntime(ctx, runtime)
  175. }
  176. func dataplaneMode(state vowifi.State) string {
  177. if value := strings.TrimSpace(state.DataplaneMode); value == "userspace" || value == "xfrm" {
  178. return value
  179. }
  180. if state.TunnelReady || state.Phase == vowifi.PhaseTunnelReady ||
  181. state.Phase == vowifi.PhaseIMSReady || state.Phase == vowifi.PhaseSMSReady {
  182. return "ipsec"
  183. }
  184. return ""
  185. }
  186. func registrationText(state vowifi.State) string {
  187. if value := strings.TrimSpace(state.IMSRegistration); value != "" {
  188. return value
  189. }
  190. if state.IMSReady {
  191. return "registered"
  192. }
  193. if state.Phase == vowifi.PhaseFailed &&
  194. strings.HasPrefix(state.LastErrorClass, "ims") {
  195. return "registration failed"
  196. }
  197. return "not registered"
  198. }
  199. func boolInt(value bool) int {
  200. if value {
  201. return 1
  202. }
  203. return 0
  204. }
  205. func marshalObject(value map[string]any) (json.RawMessage, error) {
  206. encoded, err := json.Marshal(value)
  207. if err != nil {
  208. return nil, fmt.Errorf("marshal VoWiFi runtime projection: %w", err)
  209. }
  210. return encoded, nil
  211. }