mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
235 lines
6.5 KiB
Go
235 lines
6.5 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)
|
|
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,
|
|
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,
|
|
"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
|
|
}
|