Merge pull request #7 from jiuliking/fix/plmn-23410-ims-interoperability

fix: support PLMN 234-10 IMS registration
This commit is contained in:
Meng Meng
2026-08-13 10:17:10 +08:00
committed by GitHub
5 changed files with 127 additions and 5 deletions
+6 -2
View File
@@ -715,9 +715,13 @@ func newVoWiFiOrchestrator(
return nil, fmt.Errorf("device %q IKE provider: %w", deviceConfig.ID, err)
}
imsProvider, err := ims.NewProvider(adapter, ims.Config{
// The userspace SWu data plane currently carries the protected P-CSCF
// signalling path over TCP.
// The userspace SWu data plane carries protected P-CSCF signalling over
// TCP by default. UK PLMN 234-10 exposes its P-CSCF over UDP/5060 on SWu.
Transport: "tcp",
TransportByPLMN: map[string]string{
"23410": "udp",
"234010": "udp",
},
// Some Vodafone UK SIM profiles leave AT+CSCA empty; Vodafone publishes
// this service-centre number for manual SMS setup.
SMSCenter: "+447785016005",
+24 -1
View File
@@ -38,6 +38,7 @@ type Config struct {
PCSCF string
LocalAddress string
Transport string
TransportByPLMN map[string]string
Port int
RegistrationExpiry time.Duration
TransactionTimeout time.Duration
@@ -106,6 +107,19 @@ func normalizeConfig(config Config) (Config, error) {
if config.Transport != "" && config.Transport != "udp" && config.Transport != "tcp" {
return Config{}, fmt.Errorf("ims: unsupported SIP transport %q", config.Transport)
}
transportByPLMN := make(map[string]string, len(config.TransportByPLMN))
for plmn, transport := range config.TransportByPLMN {
plmn = strings.TrimSpace(plmn)
transport = strings.ToLower(strings.TrimSpace(transport))
if !digitsBetween(plmn, 5, 6) {
return Config{}, fmt.Errorf("ims: invalid transport override PLMN %q", plmn)
}
if transport != "udp" && transport != "tcp" {
return Config{}, fmt.Errorf("ims: unsupported SIP transport %q for PLMN %s", transport, plmn)
}
transportByPLMN[plmn] = transport
}
config.TransportByPLMN = transportByPLMN
if strings.TrimSpace(config.UserAgent) == "" {
config.UserAgent = "vocat/1"
}
@@ -185,7 +199,7 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.IMSRequest)
if provider.config.PCSCF != "" && !pcscfProvenByTunnel(endpoint, tunnel.PCSCF, provider.config.Port) {
return nil, errors.New("ims: configured P-CSCF is not proven by the SWu tunnel")
}
transport := provider.config.Transport
transport := transportForIdentity(provider.config, request.Identity)
if transport == "" {
transport = transportHint
}
@@ -227,6 +241,15 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.IMSRequest)
return session, nil
}
func transportForIdentity(config Config, identity vowifi.SIMIdentity) string {
mcc := strings.TrimSpace(identity.HomeMCC)
mnc := strings.TrimSpace(identity.HomeMNC)
if transport := config.TransportByPLMN[mcc+mnc]; transport != "" {
return transport
}
return config.Transport
}
type identitySet struct {
domain string
private string
+47
View File
@@ -26,6 +26,53 @@ func (evidenceTunnel) Close(context.Context) error {
return nil
}
func TestTransportForIdentityUsesPLMNOverride(t *testing.T) {
t.Parallel()
config := Config{
Transport: "tcp",
TransportByPLMN: map[string]string{
"23410": "udp",
"234010": "udp",
},
}
if got := transportForIdentity(config, vowifi.SIMIdentity{HomeMCC: "234", HomeMNC: "10"}); got != "udp" {
t.Fatalf("PLMN 234-10 transport = %q, want udp", got)
}
if got := transportForIdentity(config, vowifi.SIMIdentity{HomeMCC: "234", HomeMNC: "010"}); got != "udp" {
t.Fatalf("zero-padded PLMN 234-010 transport = %q, want udp", got)
}
if got := transportForIdentity(config, vowifi.SIMIdentity{HomeMCC: "234", HomeMNC: "15"}); got != "tcp" {
t.Fatalf("non-overridden transport = %q, want tcp", got)
}
}
func TestTransportForIdentityPreservesLeadingZeroMNCs(t *testing.T) {
t.Parallel()
config := Config{
TransportByPLMN: map[string]string{
"31001": "udp",
"310001": "tcp",
"31000": "udp",
"310000": "tcp",
},
}
for _, test := range []struct {
mnc string
want string
}{
{mnc: "01", want: "udp"},
{mnc: "001", want: "tcp"},
{mnc: "00", want: "udp"},
{mnc: "000", want: "tcp"},
} {
if got := transportForIdentity(config, vowifi.SIMIdentity{HomeMCC: "310", HomeMNC: test.mnc}); got != test.want {
t.Errorf("PLMN 310-%s transport = %q, want %q", test.mnc, got, test.want)
}
}
}
func TestProviderRegisterAKAParseEvidenceAndClose(t *testing.T) {
for _, test := range []struct {
name string
+19 -2
View File
@@ -28,8 +28,9 @@ const (
)
var (
ErrIPSecAgreementRequired = errors.New("ims: a supported ipsec-3gpp security agreement is required")
ErrIPSecInstall = errors.New("ims: install ipsec-3gpp security associations")
ErrIPSecAgreementRequired = errors.New("ims: a supported ipsec-3gpp security agreement is required")
ErrIPSecInstall = errors.New("ims: install ipsec-3gpp security associations")
errIncompleteSecurityOffer = errors.New("ims: incomplete ipsec-3gpp security offer")
)
// IPSecSAConfig is the complete, evidence-derived 3GPP transport-mode SA set.
@@ -186,6 +187,9 @@ func parseSecurityAgreement(values []string, proposal securityProposal) (securit
if err != nil {
name := strings.ToLower(strings.TrimSpace(strings.SplitN(item, ";", 2)[0]))
if name == "ipsec-3gpp" {
if errors.Is(err, errIncompleteSecurityOffer) {
continue
}
return securityAgreement{}, fmt.Errorf(
"ims: malformed ipsec-3gpp Security-Server: %w",
err,
@@ -263,6 +267,19 @@ func parseSecurityMechanism(value string) (securityMechanism, error) {
if value := parameters["ealg"]; value != "" {
mechanism.encryption = strings.ToLower(value)
}
saParameterKeys := []string{"spi-c", "spi-s", "port-c", "port-s"}
presentSAParameters := 0
for _, key := range saParameterKeys {
if parameters[key] != "" {
presentSAParameters++
}
}
if presentSAParameters == 0 {
return securityMechanism{}, errIncompleteSecurityOffer
}
if presentSAParameters != len(saParameterKeys) {
return securityMechanism{}, errors.New("ims: partially specified Security-Server SA parameters")
}
var err error
if mechanism.spiClient, err = decimalUint32(parameters["spi-c"]); err != nil {
return securityMechanism{}, err
+31
View File
@@ -36,6 +36,31 @@ func TestParseSecurityAgreementSelectsSupportedIPSec(t *testing.T) {
}
}
func TestParseSecurityAgreementSkipsIncompleteCarrierAlternatives(t *testing.T) {
proposal := securityProposal{
spiClient: 1001,
spiServer: 1002,
portClient: 40666,
portServer: 55610,
}
incomplete := []string{
"ipsec-3gpp;q=0.100;alg=hmac-md5-96;mod=trans",
"ipsec-3gpp;q=0.200;alg=hmac-sha-1-96;ealg=des-ede3-cbc;mod=trans",
"ipsec-3gpp;q=0.300;alg=hmac-sha-1-96;ealg=aes-cbc;mod=trans",
}
selected := "ipsec-3gpp;q=1.000;alg=hmac-sha-1-96;prot=esp;mod=trans;" +
"ealg=aes-cbc;spi-c=2001;spi-s=2002;port-c=50601;port-s=50600"
values := []string{strings.Join(append(incomplete, selected), ", ")}
agreement, err := parseSecurityAgreement(values, proposal)
if err != nil {
t.Fatalf("parseSecurityAgreement() error = %v", err)
}
if agreement.selected.spiClient != 2001 || agreement.selected.spiServer != 2002 {
t.Fatalf("selected mechanism = %#v", agreement.selected)
}
}
func TestParseSecurityAgreementFailsClosed(t *testing.T) {
proposal := securityProposal{
spiClient: 1001,
@@ -73,6 +98,12 @@ func TestParseSecurityAgreementFailsClosed(t *testing.T) {
valid + ", ipsec-3gpp;q=0.200;alg=hmac-sha-1-96;alg=hmac-sha-1-96",
},
},
{
name: "partially specified SA parameters poison otherwise valid list",
values: []string{
valid + ", ipsec-3gpp;q=0.200;alg=hmac-sha-1-96;spi-c=3001",
},
},
{
name: "no offer",
values: nil,