add MCC/MNC name lookup to *3gpp_uli converter

This commit is contained in:
ionutboangiu
2026-02-13 20:35:36 +02:00
committed by Dan Christian Bogos
parent 69b940039b
commit 48a9441a39
6 changed files with 3516 additions and 24 deletions

114
data/scripts/gen_mccmnc.go Normal file
View File

@@ -0,0 +1,114 @@
//go:build ignore
// Parses Wireshark's packet-e212.c and generates utils/mccmnc_data.go.
// Run: go generate ./utils/...
package main
import (
"fmt"
"go/format"
"io"
"log"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
)
const wiresharkURL = "https://raw.githubusercontent.com/wireshark/wireshark/master/epan/dissectors/packet-e212.c"
// matches entries like { 123, "Some Name" } in C value_string arrays
var entryRe = regexp.MustCompile(`\{\s*(\d+),\s*"([^"]+)"\s*\}`)
func main() {
log.SetFlags(0)
src := fetchSource()
countries := parseTable(src, "E212_codes[]", func(code int) string {
return fmt.Sprintf("%03d", code)
})
networks := parseTable(src, "mcc_mnc_2digits_codes[]", func(code int) string {
return fmt.Sprintf("%03d-%02d", code/100, code%100)
})
// 3-digit MNC: only add if no 2-digit entry exists for the same key
for k, v := range parseTable(src, "mcc_mnc_3digits_codes[]", func(code int) string {
return fmt.Sprintf("%03d-%03d", code/1000, code%1000)
}) {
if _, ok := networks[k]; !ok {
networks[k] = v
}
}
var buf strings.Builder
buf.WriteString("// Code generated by gen_mccmnc from Wireshark's packet-e212.c; DO NOT EDIT.\n\npackage utils\n\n")
writeMap(&buf, "mccCountry", "MCC to country name (ITU-T E.212)", countries)
writeMap(&buf, "mccmncNetwork", "MCC-MNC to network/operator name (ITU-T E.212)", networks)
formatted, err := format.Source([]byte(buf.String()))
if err != nil {
log.Fatalf("gofmt: %v", err)
}
if err := os.WriteFile("mccmnc_data.go", formatted, 0644); err != nil {
log.Fatal(err)
}
fmt.Printf("generated mccmnc_data.go: %d countries, %d networks\n", len(countries), len(networks))
}
func fetchSource() string {
resp, err := http.Get(wiresharkURL)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
return string(body)
}
func parseTable(src, name string, formatKey func(int) string) map[string]string {
block := extractBlock(src, name)
if block == "" {
log.Fatalf("table %s not found", name)
}
result := make(map[string]string)
for _, m := range entryRe.FindAllStringSubmatch(block, -1) {
if m[2] == "Unassigned" || m[2] == "Unset" {
continue
}
code, _ := strconv.Atoi(m[1])
result[formatKey(code)] = m[2]
}
return result
}
// extractBlock returns the body of a C value_string array, up to its { 0, NULL } terminator.
func extractBlock(src, name string) string {
marker := "value_string " + name + " = {"
start := strings.Index(src, marker)
if start < 0 {
return ""
}
end := strings.Index(src[start:], "{ 0, NULL }")
if end < 0 {
return ""
}
return src[start : start+end]
}
func writeMap(buf *strings.Builder, varName, comment string, m map[string]string) {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Fprintf(buf, "// %s maps %s.\nvar %s = map[string]string{\n", varName, comment, varName)
for _, k := range keys {
fmt.Fprintf(buf, "\t%q: %q,\n", k, m[k])
}
buf.WriteString("}\n\n")
}

View File

@@ -128,16 +128,15 @@ Converters transform the extracted value. Chain them with ``&``::
* ``*e164Domain`` - extract domain from NAPTR record
* ``*ip2hex`` - IP to hex
* ``*sipuri_host``, ``*sipuri_user``, ``*sipuri_method`` - parse SIP URIs
* ``*3gpp_uli`` - decode 3GPP-User-Location-Info hex to ULI object
* ``*3gpp_uli`` - decode 3GPP-User-Location-Info hex to ULI object (JSON)
* ``*3gpp_uli:path`` - extract specific field from ULI
ULI component paths: ``CGI``, ``SAI``, ``RAI``, ``TAI``, ``ECGI``, ``TAI5GS``, ``NCGI``
Field paths: ``TAI.MCC``, ``TAI.MNC``, ``TAI.TAC``, ``ECGI.MCC``, ``ECGI.MNC``, ``ECGI.ECI``, ``NCGI.NCI``, etc.
Paths: ``TAI``, ``ECGI``, ``NCGI``, etc. return the component as JSON. Fields: ``TAI.MCC``, ``TAI.TAC``, ``ECGI.ECI``. Append ``.Name`` for lookup: ``TAI.MCC.Name`` (country), ``TAI.MNC.Name`` (operator).
Example::
~*req.3GPP-User-Location-Info{*3gpp_uli:TAI.MCC}
~*req.3GPP-User-Location-Info{*3gpp_uli:TAI.MCC.Name}
**Time**

View File

@@ -23,6 +23,8 @@ package general_tests
import (
"bytes"
"encoding/hex"
"encoding/json"
"strings"
"testing"
"time"
@@ -35,7 +37,6 @@ import (
)
func TestDiamULI(t *testing.T) {
// t.Skip("configuration reference for *3gpp_uli request_fields; does not verify anything")
switch *utils.DBType {
case utils.MetaInternal:
case utils.MetaMySQL, utils.MetaMongo, utils.MetaPostgres:
@@ -57,12 +58,6 @@ func TestDiamULI(t *testing.T) {
"*dryrun"
],
"request_fields": [
{
"tag": "ULI",
"path": "*cgreq.ULI",
"type": "*variable",
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info"
},
{
"tag": "DecodedULI",
"path": "*cgreq.DecodedULI",
@@ -116,6 +111,18 @@ func TestDiamULI(t *testing.T) {
"path": "*cgreq.ECGI-ECI",
"type": "*variable",
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:ECGI.ECI}"
},
{
"tag": "MCC-Name",
"path": "*cgreq.MCC-Name",
"type": "*variable",
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:TAI.MCC.Name}"
},
{
"tag": "MNC-Name",
"path": "*cgreq.MNC-Name",
"type": "*variable",
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:TAI.MNC.Name}"
}
],
"reply_fields": []
@@ -124,15 +131,14 @@ func TestDiamULI(t *testing.T) {
}
}`
buf := &bytes.Buffer{}
ng := engine.TestEngine{
ConfigJSON: cfgJSON,
DBCfg: engine.InternalDBCfg,
LogBuffer: &bytes.Buffer{},
LogBuffer: buf,
}
t.Cleanup(func() {
t.Log(ng.LogBuffer)
})
_, cfg := ng.Run(t)
time.Sleep(20 * time.Millisecond)
diamClient, err := agents.NewDiameterClient(cfg.DiameterAgentCfg().Listeners[0].Address, "localhost",
cfg.DiameterAgentCfg().OriginRealm, cfg.DiameterAgentCfg().VendorID,
@@ -143,7 +149,7 @@ func TestDiamULI(t *testing.T) {
}
// Binary ULI from Wireshark capture: TAI+ECGI, MCC=547, MNC=05, TAC=1, ECI=257
uliBytes, err := hex.DecodeString("8245f750000145f75000000101")
uliBytes, err := hex.DecodeString("8262f210000162f21000000101")
if err != nil {
t.Fatal(err)
}
@@ -163,7 +169,38 @@ func TestDiamULI(t *testing.T) {
)
if err := diamClient.SendMessage(ccr); err != nil {
t.Errorf("failed to send diameter message: %v", err)
t.Fatal(err)
}
_ = diamClient.ReceivedMessage(2 * time.Second)
expected := map[string]string{
"DecodedULI": `{"TAI":{"MCC":"262","MNC":"01","TAC":1},"ECGI":{"MCC":"262","MNC":"01","ECI":257}}`,
"TAI": `{"MCC":"262","MNC":"01","TAC":1}`,
"ECGI": `{"MCC":"262","MNC":"01","ECI":257}`,
"TAI-MCC": "262",
"TAI-MNC": "01",
"TAI-TAC": "1",
"ECGI-MCC": "262",
"ECGI-MNC": "01",
"ECGI-ECI": "257",
"MCC-Name": "Germany",
"MNC-Name": "Telekom Deutschland GmbH",
}
parts := strings.Split(buf.String(), "CGREvent: ")
if len(parts) < 2 {
t.Fatal("no CGREvent found in dryrun log")
}
var ev utils.CGREvent
if err := json.NewDecoder(strings.NewReader(parts[len(parts)-1])).Decode(&ev); err != nil {
t.Fatalf("failed to decode CGREvent: %v", err)
}
for field, want := range expected {
got := utils.IfaceAsString(ev.Event[field])
if got != want {
t.Errorf("%s: got %q, want %q", field, got, want)
}
}
}

3293
utils/mccmnc_data.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>
package utils
//go:generate go run ../data/scripts/gen_mccmnc.go
import (
"encoding/binary"
"errors"
@@ -277,9 +279,9 @@ func decodeNCGI(data []byte) *NCGI {
}
}
// GetField retrieves a value found at the specified path (e.g. "TAI.MCC" or "ECGI.ECI").
// GetField retrieves a value found at the specified path (e.g. "TAI.MCC", "ECGI.ECI", "TAI.MCC.Name").
func (uli *ULI) GetField(path string) (any, error) {
parts := strings.SplitN(path, ".", 2)
parts := strings.SplitN(path, ".", 3)
if len(parts) == 0 || parts[0] == "" {
return nil, errors.New("empty path")
}
@@ -331,17 +333,32 @@ func (uli *ULI) GetField(path string) (any, error) {
return loc, nil
}
return uliFieldValue(loc, parts[1], mcc, mnc)
}
func uliFieldValue(loc any, field, mcc, mnc string) (any, error) {
switch field {
switch parts[1] {
case "MCC":
if len(parts) == 3 {
if parts[2] == "Name" {
return countryName(mcc)
}
return nil, fmt.Errorf("unknown MCC subfield: %s", parts[2])
}
return mcc, nil
case "MNC":
if len(parts) == 3 {
if parts[2] == "Name" {
return networkName(mcc, mnc)
}
return nil, fmt.Errorf("unknown MNC subfield: %s", parts[2])
}
return mnc, nil
default:
if len(parts) == 3 {
return nil, fmt.Errorf("unknown subfield: %s.%s", parts[1], parts[2])
}
return uliFieldValue(loc, parts[1])
}
}
func uliFieldValue(loc any, field string) (any, error) {
switch l := loc.(type) {
case *CGI:
switch field {
@@ -384,3 +401,17 @@ func uliFieldValue(loc any, field, mcc, mnc string) (any, error) {
return nil, fmt.Errorf("unknown field: %s", field)
}
func countryName(mcc string) (string, error) {
if name, ok := mccCountry[mcc]; ok {
return name, nil
}
return "", fmt.Errorf("unknown MCC: %s", mcc)
}
func networkName(mcc, mnc string) (string, error) {
if name, ok := mccmncNetwork[mcc+"-"+mnc]; ok {
return name, nil
}
return "", fmt.Errorf("unknown MCC-MNC: %s-%s", mcc, mnc)
}

View File

@@ -214,6 +214,9 @@ func TestULI_GetField(t *testing.T) {
{"NCGI.MCC", "310"},
{"NCGI.MNC", "260"},
{"NCGI.NCI", uint64(0x123456789)},
{"TAI.MCC.Name", "French Polynesia"},
{"NCGI.MCC.Name", "United States"},
{"NCGI.MNC.Name", "T-Mobile USA"},
}
for _, tt := range tests {
@@ -246,6 +249,9 @@ func TestULI_GetField_Errors(t *testing.T) {
{"invalid field", "TAI.INVALID"},
{"invalid component", "INVALID"},
{"empty path", ""},
{"invalid MCC subfield", "TAI.MCC.Invalid"},
{"invalid MNC subfield", "TAI.MNC.Invalid"},
{"subfield on TAC", "TAI.TAC.Name"},
}
for _, tt := range tests {
@@ -307,6 +313,18 @@ func TestULIConverter(t *testing.T) {
hex: "871300620123456789",
expected: uint64(0x123456789),
},
{
name: "Extract TAI5GS.MCC.Name",
params: "*3gpp_uli:TAI5GS.MCC.Name",
hex: "88130062123456",
expected: "United States",
},
{
name: "Extract NCGI.MNC.Name",
params: "*3gpp_uli:NCGI.MNC.Name",
hex: "871300620123456789",
expected: "T-Mobile USA",
},
}
for _, tt := range tests {