mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-20 06:38:45 +05:00
add MCC/MNC name lookup to *3gpp_uli converter
This commit is contained in:
committed by
Dan Christian Bogos
parent
69b940039b
commit
48a9441a39
114
data/scripts/gen_mccmnc.go
Normal file
114
data/scripts/gen_mccmnc.go
Normal 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")
|
||||
}
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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
3293
utils/mccmnc_data.go
Normal file
File diff suppressed because it is too large
Load Diff
45
utils/uli.go
45
utils/uli.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user