mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-21 07:13:43 +08:00
- Added support for reading SIM metadata (SPN, GID1, GID2) in EC20 and Native QMI adapters. - Refactored ePDG resolver to utilize carrier profiles for DNS resolution. - Introduced automatic legacy proposal fallback in IKE provider based on negotiation failures. - Updated IMS provider to cache transport settings per SIM identity and implement transport fallback logic. - Enhanced SMS center retrieval to fall back to carrier profiles when no explicit configuration is found. - Updated state management to include carrier profile information. - Improved integration tests to cover new transport caching and SMS center logic. - Added UI components to display carrier profile and match source in the device overview. - Updated internationalization files to include new labels for carrier profile and match source.
290 lines
8.6 KiB
Go
290 lines
8.6 KiB
Go
// 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)
|
|
iccid := strings.TrimSpace(request.ICCID)
|
|
if deviceID == "" {
|
|
return vowifi.ProxyRoute{Mode: vowifi.ProxyModeDirect}, nil
|
|
}
|
|
var upstreamID string
|
|
matchedCountryRule := false
|
|
if iccid != "" {
|
|
binding, err := resolver.Store.DeviceProxyBinding(ctx, iccid)
|
|
if err == nil {
|
|
upstreamID = binding.UpstreamProxyID
|
|
} else if !errors.Is(err, store.ErrNotFound) {
|
|
return vowifi.ProxyRoute{}, fmt.Errorf("resolve proxy binding for ICCID %s: %w", iccid, err)
|
|
}
|
|
}
|
|
if upstreamID == "" {
|
|
country, found := device.CountryForMCC(strings.TrimSpace(request.HomeMCC))
|
|
if !found {
|
|
country = strings.ToUpper(strings.TrimSpace(request.CountryCode))
|
|
if len(country) != 2 {
|
|
return vowifi.ProxyRoute{Mode: vowifi.ProxyModeDirect}, nil
|
|
}
|
|
}
|
|
rule, ruleErr := resolver.Store.CountryRule(ctx, country)
|
|
if errors.Is(ruleErr, store.ErrNotFound) || (ruleErr == nil && !rule.Enabled) {
|
|
return vowifi.ProxyRoute{Mode: vowifi.ProxyModeDirect}, nil
|
|
}
|
|
if ruleErr != nil {
|
|
return vowifi.ProxyRoute{}, fmt.Errorf("resolve proxy country rule for MCC %s: %w", request.HomeMCC, ruleErr)
|
|
}
|
|
upstreamID = rule.UpstreamProxyID
|
|
matchedCountryRule = true
|
|
}
|
|
upstream, err := resolver.Store.UpstreamProxy(ctx, upstreamID)
|
|
if err != nil {
|
|
return vowifi.ProxyRoute{}, fmt.Errorf(
|
|
"load upstream proxy %q for device %s: %w",
|
|
upstreamID,
|
|
deviceID,
|
|
err,
|
|
)
|
|
}
|
|
if !upstream.Enabled {
|
|
if matchedCountryRule {
|
|
return vowifi.ProxyRoute{Mode: vowifi.ProxyModeDirect}, nil
|
|
}
|
|
return vowifi.ProxyRoute{}, fmt.Errorf(
|
|
"upstream proxy %q for device %s is disabled",
|
|
upstream.ID,
|
|
deviceID,
|
|
)
|
|
}
|
|
if matchedCountryRule && iccid != "" {
|
|
created, bindErr := resolver.Store.InsertDeviceProxyBindingIfAbsent(ctx, store.DeviceProxyBinding{
|
|
DeviceID: deviceID,
|
|
ICCID: iccid,
|
|
ProfileName: iccid,
|
|
UpstreamProxyID: upstream.ID,
|
|
})
|
|
if bindErr != nil {
|
|
return vowifi.ProxyRoute{}, fmt.Errorf("materialize MCC proxy route for ICCID %s: %w", iccid, bindErr)
|
|
}
|
|
if !created {
|
|
// Another request or an administrator may have created an explicit
|
|
// binding after our first lookup. The persisted ICCID route wins.
|
|
binding, bindingErr := resolver.Store.DeviceProxyBinding(ctx, iccid)
|
|
if bindingErr != nil {
|
|
return vowifi.ProxyRoute{}, fmt.Errorf("reload proxy binding for ICCID %s: %w", iccid, bindingErr)
|
|
}
|
|
if binding.UpstreamProxyID != upstream.ID {
|
|
upstream, err = resolver.Store.UpstreamProxy(ctx, binding.UpstreamProxyID)
|
|
if err != nil {
|
|
return vowifi.ProxyRoute{}, fmt.Errorf("load materialized 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,
|
|
ICCID: strings.TrimSpace(state.ICCID),
|
|
IMSI: strings.TrimSpace(state.IMSI),
|
|
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 {
|
|
// An active VoWiFi session belongs to the identity captured when it
|
|
// was established. Do not relabel its phone number with a newly
|
|
// selected eSIM profile while teardown is still in progress.
|
|
if runtime.ICCID == "" {
|
|
runtime.ICCID = strings.TrimSpace(entry.Snapshot.ICCID)
|
|
}
|
|
if runtime.IMSI == "" {
|
|
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,
|
|
"carrier_profile": state.CarrierProfile,
|
|
"carrier_profile_from": state.CarrierProfileFrom,
|
|
"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
|
|
}
|