// Package integration connects the protocol runtime to vocat's persistent // configuration and modem inventory. It contains no IKE, IMS, or SIM protocol // implementation. package integration import ( "context" "encoding/json" "errors" "fmt" "strings" "vocat/internal/device" "vocat/internal/store" "vocat/internal/vowifi" ) type ProxyResolver struct { Store *store.Store } func (resolver ProxyResolver) Resolve( ctx context.Context, request vowifi.ProxyRequest, ) (vowifi.ProxyRoute, error) { if resolver.Store == nil { return vowifi.ProxyRoute{}, errors.New("vowifi proxy resolver: store is nil") } deviceID := strings.TrimSpace(request.DeviceID) if deviceID == "" { return vowifi.ProxyRoute{Mode: vowifi.ProxyModeDirect}, nil } binding, err := resolver.Store.DeviceProxyBinding(ctx, deviceID) if errors.Is(err, store.ErrNotFound) { return vowifi.ProxyRoute{Mode: vowifi.ProxyModeDirect}, nil } if err != nil { return vowifi.ProxyRoute{}, fmt.Errorf("resolve proxy binding for device %s: %w", deviceID, err) } upstream, err := resolver.Store.UpstreamProxy(ctx, binding.UpstreamProxyID) if err != nil { return vowifi.ProxyRoute{}, fmt.Errorf( "load upstream proxy %q for device %s: %w", binding.UpstreamProxyID, deviceID, err, ) } if !upstream.Enabled { return vowifi.ProxyRoute{}, fmt.Errorf( "upstream proxy %q for device %s is disabled", upstream.ID, deviceID, ) } return vowifi.ProxyRoute{ Mode: vowifi.ProxyModeSOCKS5, ID: upstream.ID, Address: upstream.Addr, Username: upstream.Username, Password: upstream.Password, }, nil } type PhoneStore struct { Store *store.Store DeviceID string } func (phones PhoneStore) SaveAssociatedNumber( ctx context.Context, record vowifi.PhoneRecord, ) error { if phones.Store == nil { return errors.New("vowifi phone store: store is nil") } switch record.Source { case vowifi.PhoneSourceAssociatedMSISDN, vowifi.PhoneSourcePAssociatedURI: default: return fmt.Errorf("vowifi phone store: untrusted source %q", record.Source) } return phones.Store.UpsertPhoneAssociation(ctx, store.PhoneAssociation{ ICCID: record.ICCID, DeviceID: strings.TrimSpace(phones.DeviceID), Number: record.Number, Source: record.Source, UpdatedAt: record.UpdatedAt, }) } type DeviceReader interface { Get(string) (device.Device, error) } type StateProjector struct { Store *store.Store Devices DeviceReader } func (projector StateProjector) Save( ctx context.Context, state vowifi.State, ) error { if projector.Store == nil { return errors.New("vowifi state projector: store is nil") } runtime := store.VoWiFiRuntime{ DeviceID: state.DeviceID, Phase: string(state.Phase), DataplaneMode: dataplaneMode(state), SIMReady: state.SIMReady, AccessReady: state.AccessReady, TunnelReady: state.TunnelReady, IMSReady: state.IMSReady, SMSReady: state.SMSReady, RegStatus: boolInt(state.IMSReady), RegStatusText: registrationText(state), NetworkMode: "Wi-Fi", LocalPhone: state.PhoneNumber, PhoneNumberSource: state.PhoneNumberSource, LastErrorClass: state.LastErrorClass, LastError: state.LastError, LastReason: state.LastReason, UpdatedAt: state.UpdatedAt, } if projector.Devices != nil { if entry, err := projector.Devices.Get(state.DeviceID); err == nil && entry.Snapshot != nil { runtime.ICCID = strings.TrimSpace(entry.Snapshot.ICCID) runtime.IMSI = strings.TrimSpace(entry.Snapshot.IMSI) } } if runtime.LocalPhone == "" && runtime.ICCID != "" { if association, err := projector.Store.PhoneAssociation(ctx, runtime.ICCID); err == nil { runtime.LocalPhone = association.Number runtime.PhoneNumberSource = association.Source } else if !errors.Is(err, store.ErrNotFound) { return fmt.Errorf("restore associated phone number: %w", err) } } var err error runtime.Tunnel, err = marshalObject(map[string]any{ "established": state.TunnelReady, "name": state.TunnelName, "dataplane_mode": state.DataplaneMode, "epdg": state.EPDG, "proxy_mode": state.ProxyMode, "proxy_id": state.ProxyID, "security_audit": state.Security, }) if err != nil { return err } runtime.IMSCore, err = marshalObject(map[string]any{ "registered": state.IMSReady, "registration_state": state.IMSRegistration, "associated_number": runtime.LocalPhone, "number_source": runtime.PhoneNumberSource, }) if err != nil { return err } runtime.SMSIP, err = marshalObject(map[string]any{ "ready": state.SMSReady, }) if err != nil { return err } runtime.Extra, err = marshalObject(map[string]any{ "enabled": state.Enabled, "active": state.Active, "pure_airplane_policy": state.PureAirplanePolicy, "home_mcc": state.HomeMCC, "home_mnc": state.HomeMNC, "warnings": state.Warnings, "cleanup_errors": state.CleanupErrors, "attempt": state.Attempt, "sequence": state.Sequence, "started_at": state.StartedAt, }) if err != nil { return err } return projector.Store.UpsertVoWiFiRuntime(ctx, runtime) } func dataplaneMode(state vowifi.State) string { if value := strings.TrimSpace(state.DataplaneMode); value == "userspace" || value == "xfrm" { return value } if state.TunnelReady || state.Phase == vowifi.PhaseTunnelReady || state.Phase == vowifi.PhaseIMSReady || state.Phase == vowifi.PhaseSMSReady { return "ipsec" } return "" } func registrationText(state vowifi.State) string { if value := strings.TrimSpace(state.IMSRegistration); value != "" { return value } if state.IMSReady { return "registered" } if state.Phase == vowifi.PhaseFailed && strings.HasPrefix(state.LastErrorClass, "ims") { return "registration failed" } return "not registered" } func boolInt(value bool) int { if value { return 1 } return 0 } func marshalObject(value map[string]any) (json.RawMessage, error) { encoded, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("marshal VoWiFi runtime projection: %w", err) } return encoded, nil }