mirror of
https://github.com/open5gs/open5gs.git
synced 2026-10-19 09:44:04 +00:00
eir: harden equipment checks and add integration coverage
Handle EIR checks within the AMF security mode state, preserve failure policies, and clean up pending transactions when leaving the state. Prepare missing database constraints at EIR startup while preserving existing settings. Log setup failures and continue with runtime validation. Centralize numeric identity validation and improve failure diagnostics. Move EIR tests into a dedicated suite with its own configuration, covering database queries, HTTP/2 responses, and registration outcomes. Update configuration defaults, packaging, and documentation. PR: #4708
This commit is contained in:
parent
62fbd360fb
commit
416aea4b77
41 changed files with 2476 additions and 787 deletions
132
configs/eir.yaml.in
Normal file
132
configs/eir.yaml.in
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Dedicated configuration for tests/eir/eir-integration.
|
||||
# Fixtures are owned and removed by each test; the database is never dropped.
|
||||
db_uri: mongodb://localhost/open5gs
|
||||
|
||||
logger:
|
||||
|
||||
test:
|
||||
serving:
|
||||
- plmn_id:
|
||||
mcc: 999
|
||||
mnc: 70
|
||||
|
||||
global:
|
||||
parameter:
|
||||
no_scp: true
|
||||
no_sepp: true
|
||||
no_smf: true
|
||||
no_upf: true
|
||||
no_bsf: true
|
||||
no_mme: true
|
||||
no_sgwc: true
|
||||
no_sgwu: true
|
||||
no_pcrf: true
|
||||
no_hss: true
|
||||
|
||||
amf:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.5
|
||||
port: 7777
|
||||
client:
|
||||
nrf:
|
||||
- uri: http://127.0.0.10:7777
|
||||
ngap:
|
||||
server:
|
||||
- address: 127.0.0.5
|
||||
guami:
|
||||
- plmn_id:
|
||||
mcc: 999
|
||||
mnc: 70
|
||||
amf_id:
|
||||
region: 2
|
||||
set: 1
|
||||
tai:
|
||||
- plmn_id:
|
||||
mcc: 999
|
||||
mnc: 70
|
||||
tac: 1
|
||||
plmn_support:
|
||||
- plmn_id:
|
||||
mcc: 999
|
||||
mnc: 70
|
||||
s_nssai:
|
||||
- sst: 1
|
||||
security:
|
||||
integrity_order: [ NIA2, NIA1, NIA0 ]
|
||||
ciphering_order: [ NEA0, NEA1, NEA2 ]
|
||||
network_name:
|
||||
full: Open5GS EIR test
|
||||
amf_name: open5gs-amf-eir-test
|
||||
time:
|
||||
t3512:
|
||||
value: 540
|
||||
eir:
|
||||
enabled: true
|
||||
unknown_action: reject
|
||||
failure_action: reject
|
||||
missing_pei_action: reject
|
||||
|
||||
nrf:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.10
|
||||
port: 7777
|
||||
|
||||
ausf:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.11
|
||||
port: 7777
|
||||
client:
|
||||
nrf:
|
||||
- uri: http://127.0.0.10:7777
|
||||
|
||||
udm:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.12
|
||||
port: 7777
|
||||
client:
|
||||
nrf:
|
||||
- uri: http://127.0.0.10:7777
|
||||
|
||||
pcf:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.13
|
||||
port: 7777
|
||||
client:
|
||||
nrf:
|
||||
- uri: http://127.0.0.10:7777
|
||||
|
||||
nssf:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.14
|
||||
port: 7777
|
||||
client:
|
||||
nrf:
|
||||
- uri: http://127.0.0.10:7777
|
||||
nsi:
|
||||
- uri: http://127.0.0.10:7777
|
||||
s_nssai:
|
||||
sst: 1
|
||||
|
||||
udr:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.20
|
||||
port: 7777
|
||||
client:
|
||||
nrf:
|
||||
- uri: http://127.0.0.10:7777
|
||||
|
||||
eir:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.21
|
||||
port: 7777
|
||||
client:
|
||||
nrf:
|
||||
- uri: http://127.0.0.10:7777
|
||||
|
|
@ -32,6 +32,7 @@ conf_data.set('build_subprojects_freeDiameter_extensions_dir',
|
|||
|
||||
example_conf = '''
|
||||
sample.yaml
|
||||
eir.yaml
|
||||
attach.yaml
|
||||
dns.yaml
|
||||
310014.yaml
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ logger:
|
|||
path: @localstatedir@/log/open5gs/eir.log
|
||||
# level: info # fatal|error|warn|info(default)|debug|trace
|
||||
|
||||
global:
|
||||
max:
|
||||
ue: 1024 # The number of UE can be increased depending on memory size.
|
||||
# peer: 64
|
||||
|
||||
eir:
|
||||
sbi:
|
||||
server:
|
||||
|
|
|
|||
|
|
@ -334,12 +334,3 @@ udr:
|
|||
client:
|
||||
scp:
|
||||
- uri: http://127.0.0.200:7777
|
||||
|
||||
eir:
|
||||
sbi:
|
||||
server:
|
||||
- address: 127.0.0.21
|
||||
port: 7777
|
||||
client:
|
||||
scp:
|
||||
- uri: http://127.0.0.200:7777
|
||||
|
|
|
|||
8
debian/control
vendored
8
debian/control
vendored
|
|
@ -305,7 +305,7 @@ Depends: ${misc:Depends},
|
|||
open5gs-nssf (= ${binary:Version}),
|
||||
open5gs-bsf (= ${binary:Version}),
|
||||
open5gs-udr (= ${binary:Version})
|
||||
Recommends:
|
||||
Suggests:
|
||||
open5gs-eir (= ${binary:Version})
|
||||
Description: 5G Core and EPC (metapackage)
|
||||
Open5GS is a C-language implementation of 5G Core and EPC
|
||||
|
|
@ -313,10 +313,8 @@ Description: 5G Core and EPC (metapackage)
|
|||
.
|
||||
This is a metapackage that depends on all the open5gs parts.
|
||||
.
|
||||
open5gs-eir (5G-EIR) is a Recommends, not a Depends: the equipment
|
||||
identity check it provides is disabled in the AMF by default
|
||||
(amf.eir.enabled: false), so it is optional signaling functionality
|
||||
rather than a required part of the core.
|
||||
Install open5gs-eir separately to enable optional equipment identity
|
||||
checks. The AMF consumer is disabled by default (amf.eir.enabled: false).
|
||||
|
||||
Package: open5gs-dbg
|
||||
Architecture: any
|
||||
|
|
|
|||
|
|
@ -39,15 +39,30 @@ None of this touches the SIM/subscriber side of registration — it's purely a c
|
|||
|
||||
### Step 1 — Start the 5G-EIR
|
||||
|
||||
It uses the same MongoDB database as the rest of Open5GS, so there's nothing extra to install or configure beyond a normal Open5GS setup:
|
||||
For Debian/Ubuntu packages, install the optional NF explicitly:
|
||||
|
||||
```bash
|
||||
$ sudo apt install open5gs-eir
|
||||
```
|
||||
|
||||
It uses the same MongoDB database as the rest of Open5GS. For a source installation, start it with:
|
||||
|
||||
```bash
|
||||
$ open5gs-eird -c /etc/open5gs/eir.yaml
|
||||
```
|
||||
|
||||
On startup, the 5G-EIR automatically prepares missing indexes and a basic
|
||||
validator in the configured MongoDB database. Existing indexes and validation
|
||||
settings are preserved. If preparation fails, a warning identifies the problem
|
||||
and EIR continues with runtime record validation; check these warnings before
|
||||
provisioning devices. See [Data integrity](#data-integrity) for customization.
|
||||
|
||||
### Step 2 — Add a device to the list
|
||||
|
||||
Records live in MongoDB, in a collection called `eir`. You can add them with `mongosh` (or any MongoDB tool/GUI you're comfortable with):
|
||||
Records live in MongoDB, in a collection called `eir`. Connect to the database
|
||||
configured in `eir.yaml`, for example `mongosh mongodb://localhost/open5gs`.
|
||||
With the default constraints prepared at startup, you can add records directly
|
||||
with `mongosh` (or another MongoDB tool):
|
||||
|
||||
```javascript
|
||||
// Block a specific device (by its IMEISV), for every subscriber
|
||||
|
|
@ -94,6 +109,11 @@ The safe starting point for most deployments is to leave everything on `allow`:
|
|||
|
||||
A `BLACKLISTED` device is **always** rejected, regardless of these settings — that one is never in doubt.
|
||||
|
||||
Blacklist rejection uses 5GMM cause #6 (`Illegal ME`). Rejection under
|
||||
`unknown_action` or `missing_pei_action` uses #7 (`5GS services not allowed`).
|
||||
Rejection under `failure_action` uses #90 (`Payload was not forwarded`),
|
||||
including request transmission failures, timeouts, and invalid EIR responses.
|
||||
|
||||
## 5. Going further: per-subscriber overrides
|
||||
---
|
||||
|
||||
|
|
@ -126,15 +146,103 @@ The details below are for operators writing tooling around the `eir` collection,
|
|||
2. A `pei`-only match (a record with `supi` missing or `null`).
|
||||
3. If neither exists, the device is treated as unknown (`404`, cause `ERROR_EQUIPMENT_UNKNOWN`).
|
||||
|
||||
A record with an unrecognized `status`, or more than one record matching the same lookup, is treated as a database error — never as an implicit allow.
|
||||
A record with an unrecognized `status`, or more than one record matching the same
|
||||
lookup, causes an HTTP `500` response. A malformed subscriber-specific match does
|
||||
not fall back to a general record. The AMF applies `failure_action` to these errors;
|
||||
with `allow`, registration can still proceed.
|
||||
|
||||
**Data integrity.** `open5gs-eird` automatically sets up, at startup:
|
||||
- A unique index so at most one subscriber-specific record can exist per `(pei, supi)` pair.
|
||||
- A unique index so at most one general record can exist per `pei`.
|
||||
- A schema validator requiring `pei` (string) and `status` (one of `WHITELISTED`/`BLACKLISTED`/`GREYLISTED`), rejecting malformed writes at the database level.
|
||||
### Data integrity
|
||||
|
||||
A general (device-only) record's `supi` field must be either omitted or explicitly `null` (not an empty string) — both are treated identically.
|
||||
After connecting to the database configured in `eir.yaml`, the 5G-EIR checks and
|
||||
prepares its collection before opening its SBI service. Only the EIR NF performs
|
||||
this setup; starting other Open5GS NFs does not create EIR constraints.
|
||||
|
||||
- If the `eir` collection is absent, EIR creates it with the default validator.
|
||||
- If a suitable unique index on `(pei, supi)` is absent, EIR attempts to add it.
|
||||
An equivalent existing index is accepted even when its name differs.
|
||||
- If an existing collection has no validator or explicit validation settings,
|
||||
EIR attempts to add the default validator.
|
||||
- Existing indexes are never dropped or changed. Existing validators and
|
||||
explicit validation settings are preserved, including an empty validator,
|
||||
`validationLevel: "off"`, or `validationAction: "warn"`.
|
||||
|
||||
**Default unique index.** EIR creates the following index when needed. You do not
|
||||
need to run this command after successful automatic preparation; it is shown for
|
||||
reference and for operators who prepare the database themselves:
|
||||
|
||||
```javascript
|
||||
db.eir.createIndex(
|
||||
{ pei: 1, supi: 1 },
|
||||
{ name: "pei_supi_unique", unique: true, collation: { locale: "simple" } }
|
||||
)
|
||||
```
|
||||
|
||||
This supports the lookup queries and prevents concurrent writers from creating
|
||||
multiple records for the same `(pei, supi)`. It allows both a general record and
|
||||
subscriber-specific overrides for a PEI. A missing `supi` and an explicit `null`
|
||||
share the same index key, allowing only one general record per PEI. Do not use an
|
||||
empty string for a general record's SUPI.
|
||||
|
||||
For an existing collection, review its indexes with `db.eir.getIndexes()` and
|
||||
resolve duplicate records if automatic index creation fails. An older
|
||||
non-partial unique index on `pei` alone prevents subscriber-specific overrides;
|
||||
replacing that index requires an explicit operator migration. An index that
|
||||
applies only to some records or uses different comparison rules is not assumed
|
||||
to provide the default uniqueness constraint.
|
||||
|
||||
**Default validator and customization.** The default validator requires a string
|
||||
PEI and a supported status, and permits a string, omitted, or null SUPI. EIR adds
|
||||
these rules automatically when validation settings are absent. You can use the
|
||||
following command to install or customize the rules yourself:
|
||||
|
||||
```javascript
|
||||
db.runCommand({
|
||||
collMod: "eir",
|
||||
validator: {
|
||||
$jsonSchema: {
|
||||
bsonType: "object",
|
||||
required: ["pei", "status"],
|
||||
properties: {
|
||||
pei: { bsonType: "string" },
|
||||
supi: { bsonType: ["string", "null"] },
|
||||
status: { enum: ["WHITELISTED", "BLACKLISTED", "GREYLISTED"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
validationLevel: "strict",
|
||||
validationAction: "error"
|
||||
})
|
||||
```
|
||||
|
||||
This command replaces the collection's existing validator. Inspect
|
||||
`db.getCollectionInfos({ name: "eir" })` first and preserve any additional
|
||||
operator rules. Your validator and validation settings remain in place across
|
||||
EIR restarts. Complete schema customization before starting EIR: checking for
|
||||
missing settings and adding them are separate operations, so concurrent schema
|
||||
changes during startup are not coordinated.
|
||||
|
||||
Automatic preparation needs metadata-read permissions and, when settings are
|
||||
missing, permission to create the collection or indexes and modify validation.
|
||||
You can instead prepare the database using an administration account and run EIR
|
||||
with record-read and metadata-read permissions. A metadata lookup failure is
|
||||
reported as a failure, never interpreted as an absent index or validator.
|
||||
|
||||
If checking or creating constraints fails, EIR logs a warning and continues.
|
||||
INFO logs identify constraints prepared at startup and existing settings that
|
||||
were preserved. Check these messages together with any setup warnings.
|
||||
Preparation commands limit server-side work to five seconds. For a large
|
||||
existing collection, create the index manually if this limit is exceeded.
|
||||
Correct missing permissions, duplicate records, or conflicting settings, then
|
||||
restart EIR or complete the setup manually. A database connection failure still
|
||||
prevents startup. Successful EIR startup alone does not guarantee that every
|
||||
constraint was installed.
|
||||
|
||||
Neither startup nor the validator repairs or removes existing invalid records.
|
||||
Runtime checks remain necessary for legacy or imported data. Without a suitable
|
||||
index, lookups may scan the collection, and runtime duplicate detection cannot
|
||||
prevent concurrent conflicting writes. Provisioning tools must also validate
|
||||
the supported PEI/SUPI formats described below.
|
||||
|
||||
**Registration timing.** The check runs once per initial registration attempt, after NAS security is established and the device's PEI has been obtained, and before Registration Accept is sent. It does not run on Service Request.
|
||||
|
||||
**Current limitations.** This implementation covers PEI/SUPI-exact matching and the three standard statuses. It does not (yet) include: EPC/MME S13, Diameter EIR, CEIR federation, TAC-range/wildcard rules, bulk import, or a WebUI — records are managed directly in MongoDB for now.
|
||||
**Current limitations.** This implementation supports `imei-` (15 digits), `imeisv-` (16 digits), and IMSI-based SUPIs (`imsi-`, 6–15 digits). Matching uses the supplied PEI string exactly; IMEI and IMEISV are not normalized to the same device identity. GPSI-based lookup, other PEI/SUPI formats, and optional feature negotiation are not implemented. It does not include EPC/MME S13, Diameter EIR, CEIR federation, TAC-range/wildcard rules, bulk import, or a WebUI for managing `eir` records.
|
||||
|
|
|
|||
383
lib/dbi/eir.c
383
lib/dbi/eir.c
|
|
@ -19,75 +19,310 @@
|
|||
|
||||
#include "ogs-dbi.h"
|
||||
|
||||
static bool value_is_all_digits(const char *value)
|
||||
#define EIR_SETUP_MAX_TIME_MS 5000
|
||||
|
||||
static int eir_collection_info(bool *exists, bool *validation)
|
||||
{
|
||||
size_t i;
|
||||
int rv = OGS_OK;
|
||||
bson_t *opts;
|
||||
const bson_t *document;
|
||||
bson_iter_t iter, options;
|
||||
bson_error_t error;
|
||||
mongoc_cursor_t *cursor;
|
||||
|
||||
if (!value || !value[0])
|
||||
*exists = false;
|
||||
*validation = false;
|
||||
opts = BCON_NEW("filter", "{", "name", BCON_UTF8("eir"), "}",
|
||||
"maxTimeMS", BCON_INT32(EIR_SETUP_MAX_TIME_MS));
|
||||
cursor = mongoc_database_find_collections_with_opts(
|
||||
ogs_mongoc()->database, opts);
|
||||
bson_destroy(opts);
|
||||
|
||||
while (mongoc_cursor_next(cursor, &document)) {
|
||||
if (*exists) {
|
||||
ogs_error("Multiple EIR collection metadata records returned");
|
||||
rv = OGS_ERROR;
|
||||
break;
|
||||
}
|
||||
if (!bson_iter_init_find(&iter, document, "name") ||
|
||||
!BSON_ITER_HOLDS_UTF8(&iter) ||
|
||||
strcmp(bson_iter_utf8(&iter, NULL), "eir")) {
|
||||
ogs_error("Missing or unexpected EIR collection metadata name");
|
||||
rv = OGS_ERROR;
|
||||
break;
|
||||
}
|
||||
if (!bson_iter_init_find(&iter, document, "type") ||
|
||||
!BSON_ITER_HOLDS_UTF8(&iter) ||
|
||||
strcmp(bson_iter_utf8(&iter, NULL), "collection")) {
|
||||
ogs_error("EIR namespace is not a regular collection");
|
||||
rv = OGS_ERROR;
|
||||
break;
|
||||
}
|
||||
if (!bson_iter_init_find(&iter, document, "options") ||
|
||||
!BSON_ITER_HOLDS_DOCUMENT(&iter) ||
|
||||
!bson_iter_recurse(&iter, &options)) {
|
||||
ogs_error("Missing or invalid EIR collection options");
|
||||
rv = OGS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
*exists = true;
|
||||
while (bson_iter_next(&options)) {
|
||||
const char *key = bson_iter_key(&options);
|
||||
|
||||
/* Preserve explicit empty validators and disabled validation. */
|
||||
if (!strcmp(key, "validator") ||
|
||||
!strcmp(key, "validationLevel") ||
|
||||
!strcmp(key, "validationAction"))
|
||||
*validation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (mongoc_cursor_error(cursor, &error)) {
|
||||
ogs_error("Cannot inspect EIR collection: %s", error.message);
|
||||
rv = OGS_ERROR;
|
||||
}
|
||||
mongoc_cursor_destroy(cursor);
|
||||
|
||||
if (rv == OGS_OK)
|
||||
ogs_debug("EIR collection metadata [exists:%d,validation:%d]",
|
||||
*exists, *validation);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
static bool eir_set_validator(bool create, bson_error_t *error)
|
||||
{
|
||||
bool ok;
|
||||
bson_t *command;
|
||||
|
||||
command = BCON_NEW(create ? "create" : "collMod", BCON_UTF8("eir"),
|
||||
"validator", "{", "$jsonSchema", "{",
|
||||
"bsonType", BCON_UTF8("object"),
|
||||
"required", "[", BCON_UTF8(OGS_EIR_PEI_STRING),
|
||||
BCON_UTF8(OGS_EIR_STATUS_STRING), "]",
|
||||
"properties", "{",
|
||||
OGS_EIR_PEI_STRING, "{",
|
||||
"bsonType", BCON_UTF8("string"), "}",
|
||||
OGS_EIR_SUPI_STRING, "{", "bsonType", "[",
|
||||
BCON_UTF8("string"), BCON_UTF8("null"), "]", "}",
|
||||
OGS_EIR_STATUS_STRING, "{", "enum", "[",
|
||||
BCON_UTF8("WHITELISTED"),
|
||||
BCON_UTF8("BLACKLISTED"),
|
||||
BCON_UTF8("GREYLISTED"), "]", "}",
|
||||
"}",
|
||||
"}", "}",
|
||||
"validationLevel", BCON_UTF8("strict"),
|
||||
"validationAction", BCON_UTF8("error"),
|
||||
"maxTimeMS", BCON_INT32(EIR_SETUP_MAX_TIME_MS));
|
||||
ok = mongoc_database_command_simple(
|
||||
ogs_mongoc()->database, command, NULL, NULL, error);
|
||||
bson_destroy(command);
|
||||
|
||||
if (ok) {
|
||||
if (create)
|
||||
ogs_info("Created EIR collection with default validator");
|
||||
else
|
||||
ogs_info("Installed default EIR validator");
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool eir_index_is_unique(const bson_t *document)
|
||||
{
|
||||
bson_iter_t iter, key, collation;
|
||||
const char *second_key;
|
||||
|
||||
if (!bson_iter_init_find(&iter, document, "unique") ||
|
||||
!BSON_ITER_HOLDS_BOOL(&iter) || !bson_iter_bool(&iter)) {
|
||||
ogs_warn("EIR index has no explicit unique option");
|
||||
return false;
|
||||
}
|
||||
if (bson_has_field(document, "partialFilterExpression")) {
|
||||
ogs_error("EIR partial index does not cover all equipment records");
|
||||
return false;
|
||||
}
|
||||
if (!bson_iter_init_find(&iter, document, "key") ||
|
||||
!BSON_ITER_HOLDS_DOCUMENT(&iter) ||
|
||||
!bson_iter_recurse(&iter, &key)) {
|
||||
ogs_error("EIR index has missing or invalid key metadata");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i = 0; value[i]; i++)
|
||||
if (!isdigit((unsigned char)value[i]))
|
||||
if (!bson_iter_next(&key)) {
|
||||
ogs_error("EIR index has no key fields");
|
||||
return false;
|
||||
}
|
||||
if (!strcmp(bson_iter_key(&key), OGS_EIR_PEI_STRING))
|
||||
second_key = OGS_EIR_SUPI_STRING;
|
||||
else if (!strcmp(bson_iter_key(&key), OGS_EIR_SUPI_STRING))
|
||||
second_key = OGS_EIR_PEI_STRING;
|
||||
else {
|
||||
ogs_error("EIR index does not begin with PEI or SUPI");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BSON_ITER_HOLDS_NUMBER(&key) ||
|
||||
(bson_iter_as_int64(&key) != 1 &&
|
||||
bson_iter_as_int64(&key) != -1) ||
|
||||
!bson_iter_next(&key) ||
|
||||
strcmp(bson_iter_key(&key), second_key) ||
|
||||
!BSON_ITER_HOLDS_NUMBER(&key) ||
|
||||
(bson_iter_as_int64(&key) != 1 &&
|
||||
bson_iter_as_int64(&key) != -1) || bson_iter_next(&key)) {
|
||||
ogs_error("EIR index does not have exactly the PEI and SUPI keys");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bson_iter_init_find(&iter, document, "collation")) {
|
||||
if (!BSON_ITER_HOLDS_DOCUMENT(&iter) ||
|
||||
!bson_iter_recurse(&iter, &collation) ||
|
||||
!bson_iter_find(&collation, "locale") ||
|
||||
!BSON_ITER_HOLDS_UTF8(&collation) ||
|
||||
strcmp(bson_iter_utf8(&collation, NULL), "simple")) {
|
||||
ogs_error("EIR index does not use simple collation");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A compound sparse index includes every valid EIR record: at least
|
||||
* pei exists, including general records with missing or null supi.
|
||||
* Hidden indexes also continue to enforce uniqueness.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
/* IMEI: TAC(8) + serial(6) + check digit(1) = 15 digits. */
|
||||
#define OGS_EIR_IMEI_DIGITS 15
|
||||
/* IMEISV: TAC(8) + serial(6) + software version(2) = 16 digits. */
|
||||
#define OGS_EIR_IMEISV_DIGITS 16
|
||||
/* IMSI: MCC(3) + MNC(2-3) + MSIN, up to 15 digits total. */
|
||||
#define OGS_EIR_IMSI_MIN_DIGITS 6
|
||||
#define OGS_EIR_IMSI_MAX_DIGITS 15
|
||||
|
||||
bool ogs_dbi_eir_pei_is_valid(const char *pei)
|
||||
static int eir_find_unique_index(bool *found)
|
||||
{
|
||||
char *type = NULL, *value = NULL;
|
||||
bool rc = false;
|
||||
int rv = OGS_OK;
|
||||
bson_t *opts;
|
||||
const bson_t *document;
|
||||
bson_error_t error;
|
||||
mongoc_cursor_t *cursor;
|
||||
|
||||
if (!pei || !ogs_id_get_type_value(pei, &type, &value))
|
||||
goto cleanup;
|
||||
*found = false;
|
||||
opts = BCON_NEW("maxTimeMS", BCON_INT32(EIR_SETUP_MAX_TIME_MS));
|
||||
cursor = mongoc_collection_find_indexes_with_opts(
|
||||
ogs_mongoc()->collection.eir, opts);
|
||||
bson_destroy(opts);
|
||||
|
||||
if (!value_is_all_digits(value))
|
||||
goto cleanup;
|
||||
while (mongoc_cursor_next(cursor, &document)) {
|
||||
if (eir_index_is_unique(document))
|
||||
*found = true;
|
||||
}
|
||||
if (mongoc_cursor_error(cursor, &error)) {
|
||||
ogs_error("Cannot inspect EIR indexes: %s", error.message);
|
||||
rv = OGS_ERROR;
|
||||
}
|
||||
mongoc_cursor_destroy(cursor);
|
||||
|
||||
if (!strcmp(type, "imei"))
|
||||
rc = (strlen(value) == OGS_EIR_IMEI_DIGITS);
|
||||
else if (!strcmp(type, "imeisv"))
|
||||
rc = (strlen(value) == OGS_EIR_IMEISV_DIGITS);
|
||||
|
||||
cleanup:
|
||||
ogs_free(type);
|
||||
ogs_free(value);
|
||||
return rc;
|
||||
return rv;
|
||||
}
|
||||
|
||||
bool ogs_dbi_eir_supi_is_valid(const char *supi)
|
||||
static int eir_create_index(void)
|
||||
{
|
||||
char *type = NULL, *value = NULL;
|
||||
bool rc = false;
|
||||
bool found, ok;
|
||||
bson_t *command;
|
||||
bson_error_t error;
|
||||
|
||||
if (!supi || !ogs_id_get_type_value(supi, &type, &value))
|
||||
goto cleanup;
|
||||
if (eir_find_unique_index(&found) != OGS_OK) {
|
||||
ogs_error("eir_find_unique_index() failed");
|
||||
return OGS_ERROR;
|
||||
}
|
||||
if (found) {
|
||||
ogs_info("Preserving existing EIR PEI/SUPI unique index");
|
||||
return OGS_OK;
|
||||
}
|
||||
|
||||
if (!value_is_all_digits(value))
|
||||
goto cleanup;
|
||||
ogs_debug("No suitable EIR unique index; creating pei_supi_unique");
|
||||
|
||||
if (!strcmp(type, "imsi"))
|
||||
rc = (strlen(value) >= OGS_EIR_IMSI_MIN_DIGITS &&
|
||||
strlen(value) <= OGS_EIR_IMSI_MAX_DIGITS);
|
||||
command = BCON_NEW("createIndexes", BCON_UTF8("eir"),
|
||||
"indexes", "[", "{",
|
||||
"key", "{", OGS_EIR_PEI_STRING, BCON_INT32(1),
|
||||
OGS_EIR_SUPI_STRING, BCON_INT32(1), "}",
|
||||
"name", BCON_UTF8("pei_supi_unique"),
|
||||
"unique", BCON_BOOL(true),
|
||||
"collation", "{", "locale", BCON_UTF8("simple"), "}",
|
||||
"}", "]",
|
||||
"maxTimeMS", BCON_INT32(EIR_SETUP_MAX_TIME_MS));
|
||||
ok = mongoc_database_command_simple(
|
||||
ogs_mongoc()->database, command, NULL, NULL, &error);
|
||||
bson_destroy(command);
|
||||
|
||||
cleanup:
|
||||
ogs_free(type);
|
||||
ogs_free(value);
|
||||
return rc;
|
||||
if (!ok) {
|
||||
/* Another EIR may have created an equivalent index meanwhile. */
|
||||
if (eir_find_unique_index(&found) == OGS_OK && found) {
|
||||
ogs_info("EIR unique index found after concurrent creation");
|
||||
return OGS_OK;
|
||||
}
|
||||
ogs_error("Cannot create EIR unique index: %s", error.message);
|
||||
return OGS_ERROR;
|
||||
}
|
||||
|
||||
ogs_info("EIR unique index pei_supi_unique is ready");
|
||||
return OGS_OK;
|
||||
}
|
||||
|
||||
int ogs_dbi_eir_init(void)
|
||||
{
|
||||
int rv = OGS_OK;
|
||||
bool exists, validation;
|
||||
bson_error_t error;
|
||||
|
||||
if (eir_collection_info(&exists, &validation) != OGS_OK) {
|
||||
ogs_error("eir_collection_info() failed");
|
||||
return OGS_ERROR;
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
ogs_debug("Preserving existing EIR collection");
|
||||
if (validation)
|
||||
ogs_info("Preserving existing EIR validation settings");
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
/* Create the collection with validation before creating indexes. */
|
||||
if (eir_set_validator(true, &error)) {
|
||||
exists = true;
|
||||
validation = true;
|
||||
} else if (eir_collection_info(&exists, &validation) != OGS_OK ||
|
||||
!exists) {
|
||||
ogs_error("Cannot create EIR collection: %s", error.message);
|
||||
return OGS_ERROR;
|
||||
} else {
|
||||
ogs_info("Preserving concurrently created EIR collection");
|
||||
if (validation)
|
||||
ogs_info("Preserving existing EIR validation settings");
|
||||
}
|
||||
}
|
||||
|
||||
if (!validation && !eir_set_validator(false, &error)) {
|
||||
/* Recheck a concurrent initializer; do not ignore other failures. */
|
||||
if (eir_collection_info(&exists, &validation) != OGS_OK ||
|
||||
!exists || !validation) {
|
||||
ogs_error("Cannot install EIR validator: %s", error.message);
|
||||
rv = OGS_ERROR;
|
||||
} else {
|
||||
ogs_info("Preserving concurrently installed EIR validation");
|
||||
}
|
||||
}
|
||||
|
||||
if (eir_create_index() != OGS_OK) {
|
||||
ogs_error("eir_create_index() failed");
|
||||
rv = OGS_ERROR;
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
static ogs_dbi_eir_status_t eir_status_from_string(const char *status)
|
||||
{
|
||||
if (!status)
|
||||
if (!status) {
|
||||
ogs_error("Missing EIR equipment status string");
|
||||
return OGS_DBI_EIR_STATUS_UNKNOWN;
|
||||
}
|
||||
if (!strcmp(status, "WHITELISTED"))
|
||||
return OGS_DBI_EIR_STATUS_WHITELISTED;
|
||||
if (!strcmp(status, "BLACKLISTED"))
|
||||
|
|
@ -95,6 +330,7 @@ static ogs_dbi_eir_status_t eir_status_from_string(const char *status)
|
|||
if (!strcmp(status, "GREYLISTED"))
|
||||
return OGS_DBI_EIR_STATUS_GREYLISTED;
|
||||
|
||||
ogs_warn("Unsupported EIR equipment status string");
|
||||
return OGS_DBI_EIR_STATUS_UNKNOWN;
|
||||
}
|
||||
|
||||
|
|
@ -118,9 +354,10 @@ static int eir_find_one(bson_t *query, ogs_dbi_eir_record_t *record)
|
|||
|
||||
if (!mongoc_cursor_next(cursor, &document)) {
|
||||
if (mongoc_cursor_error(cursor, &error)) {
|
||||
ogs_error("Cursor Failure: %s", error.message);
|
||||
ogs_error("Cannot query EIR records: %s", error.message);
|
||||
rv = OGS_ERROR;
|
||||
} else {
|
||||
ogs_warn("No EIR record matches the equipment query");
|
||||
rv = OGS_NOTFOUND;
|
||||
}
|
||||
goto out;
|
||||
|
|
@ -137,23 +374,39 @@ static int eir_find_one(bson_t *query, ogs_dbi_eir_record_t *record)
|
|||
while (bson_iter_next(&iter)) {
|
||||
const char *key = bson_iter_key(&iter);
|
||||
|
||||
if (!strcmp(key, OGS_EIR_PEI_STRING) &&
|
||||
BSON_ITER_HOLDS_UTF8(&iter)) {
|
||||
utf8 = bson_iter_utf8(&iter, &length);
|
||||
record->pei = ogs_strndup(utf8, length);
|
||||
} else if (!strcmp(key, OGS_EIR_SUPI_STRING) &&
|
||||
BSON_ITER_HOLDS_UTF8(&iter)) {
|
||||
utf8 = bson_iter_utf8(&iter, &length);
|
||||
record->supi = ogs_strndup(utf8, length);
|
||||
} else if (!strcmp(key, OGS_EIR_STATUS_STRING) &&
|
||||
BSON_ITER_HOLDS_UTF8(&iter)) {
|
||||
utf8 = bson_iter_utf8(&iter, &length);
|
||||
record->status = eir_status_from_string(utf8);
|
||||
if (!strcmp(key, OGS_EIR_PEI_STRING)) {
|
||||
if (BSON_ITER_HOLDS_UTF8(&iter)) {
|
||||
utf8 = bson_iter_utf8(&iter, &length);
|
||||
record->pei = ogs_strndup(utf8, length);
|
||||
} else {
|
||||
ogs_warn("Invalid EIR PEI field type [%d]",
|
||||
bson_iter_type(&iter));
|
||||
}
|
||||
} else if (!strcmp(key, OGS_EIR_SUPI_STRING)) {
|
||||
if (BSON_ITER_HOLDS_UTF8(&iter)) {
|
||||
utf8 = bson_iter_utf8(&iter, &length);
|
||||
record->supi = ogs_strndup(utf8, length);
|
||||
} else if (!BSON_ITER_HOLDS_NULL(&iter)) {
|
||||
/* MongoDB equality also matches elements of an array. */
|
||||
ogs_warn("Invalid EIR SUPI field type [%d]",
|
||||
bson_iter_type(&iter));
|
||||
ogs_dbi_eir_record_free(record);
|
||||
rv = OGS_ERROR;
|
||||
goto out;
|
||||
}
|
||||
} else if (!strcmp(key, OGS_EIR_STATUS_STRING)) {
|
||||
if (BSON_ITER_HOLDS_UTF8(&iter)) {
|
||||
utf8 = bson_iter_utf8(&iter, &length);
|
||||
record->status = eir_status_from_string(utf8);
|
||||
} else {
|
||||
ogs_warn("Invalid EIR status field type [%d]",
|
||||
bson_iter_type(&iter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!record->pei || record->status == OGS_DBI_EIR_STATUS_UNKNOWN) {
|
||||
ogs_error("Invalid EIR record [pei:%s,status:%d]",
|
||||
ogs_warn("Invalid EIR record [pei:%s,status:%d]",
|
||||
record->pei ? record->pei : "NULL", record->status);
|
||||
ogs_dbi_eir_record_free(record);
|
||||
rv = OGS_ERROR;
|
||||
|
|
@ -168,7 +421,7 @@ static int eir_find_one(bson_t *query, ogs_dbi_eir_record_t *record)
|
|||
}
|
||||
|
||||
if (mongoc_cursor_error(cursor, &error)) {
|
||||
ogs_error("Cursor Failure: %s", error.message);
|
||||
ogs_error("Cannot check EIR record uniqueness: %s", error.message);
|
||||
ogs_dbi_eir_record_free(record);
|
||||
rv = OGS_ERROR;
|
||||
goto out;
|
||||
|
|
@ -192,6 +445,8 @@ int ogs_dbi_eir_check_equipment(
|
|||
ogs_assert(pei);
|
||||
ogs_assert(record);
|
||||
|
||||
memset(record, 0, sizeof(*record));
|
||||
|
||||
if (supi) {
|
||||
query = BCON_NEW(
|
||||
OGS_EIR_PEI_STRING, BCON_UTF8(pei),
|
||||
|
|
@ -199,15 +454,17 @@ int ogs_dbi_eir_check_equipment(
|
|||
rv = eir_find_one(query, record);
|
||||
bson_destroy(query);
|
||||
|
||||
if (rv != OGS_NOTFOUND)
|
||||
if (rv != OGS_NOTFOUND) {
|
||||
ogs_warn("No EIR record matches the equipment query");
|
||||
return rv;
|
||||
}
|
||||
|
||||
ogs_debug("No EIR PEI/SUPI record; trying generic equipment record");
|
||||
}
|
||||
|
||||
/*
|
||||
* Matches both a missing `supi` field and an explicit `supi: null`,
|
||||
* per MongoDB equality-match semantics; kept consistent with the
|
||||
* eir_generic_unique partial index in ogs-mongoc.c, which cannot use
|
||||
* $exists: false (unsupported in partialFilterExpression).
|
||||
* per MongoDB equality-match semantics.
|
||||
*/
|
||||
query = BCON_NEW(
|
||||
OGS_EIR_PEI_STRING, BCON_UTF8(pei),
|
||||
|
|
|
|||
|
|
@ -50,13 +50,12 @@ typedef struct ogs_dbi_eir_record_s {
|
|||
ogs_dbi_eir_status_t status;
|
||||
} ogs_dbi_eir_record_t;
|
||||
|
||||
/* EIR-only, best-effort setup; the caller may continue on OGS_ERROR. */
|
||||
int ogs_dbi_eir_init(void);
|
||||
int ogs_dbi_eir_check_equipment(
|
||||
const char *supi, const char *pei, ogs_dbi_eir_record_t *record);
|
||||
void ogs_dbi_eir_record_free(ogs_dbi_eir_record_t *record);
|
||||
|
||||
bool ogs_dbi_eir_pei_is_valid(const char *pei);
|
||||
bool ogs_dbi_eir_supi_is_valid(const char *supi);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -156,100 +156,6 @@ ogs_mongoc_t *ogs_mongoc(void)
|
|||
return &self;
|
||||
}
|
||||
|
||||
/*
|
||||
* Guarantees uniqueness of `eir` records at the database level, since the
|
||||
* collection allows both a generic (PEI-only) record and a more specific
|
||||
* (PEI+SUPI) record for the same PEI, and ogs_dbi_eir_check_equipment()
|
||||
* only detects duplicates it happens to encounter during a lookup.
|
||||
*/
|
||||
static void eir_create_indexes(mongoc_collection_t *collection)
|
||||
{
|
||||
bson_t *keys1, *partial1;
|
||||
bson_t *keys2, *partial2;
|
||||
mongoc_index_opt_t opt1, opt2;
|
||||
bson_t reply;
|
||||
bson_error_t error;
|
||||
|
||||
ogs_assert(collection);
|
||||
|
||||
keys1 = BCON_NEW("pei", BCON_INT32(1), "supi", BCON_INT32(1));
|
||||
partial1 = BCON_NEW("supi", "{", "$type", BCON_UTF8("string"), "}");
|
||||
mongoc_index_opt_init(&opt1);
|
||||
opt1.unique = true;
|
||||
opt1.name = "eir_specific_unique";
|
||||
opt1.partial_filter_expression = partial1;
|
||||
|
||||
keys2 = BCON_NEW("pei", BCON_INT32(1));
|
||||
partial2 = BCON_NEW("supi", "{", "$eq", BCON_NULL, "}");
|
||||
mongoc_index_opt_init(&opt2);
|
||||
opt2.unique = true;
|
||||
opt2.name = "eir_generic_unique";
|
||||
opt2.partial_filter_expression = partial2;
|
||||
|
||||
if (!mongoc_collection_create_index_with_opts(
|
||||
collection, keys1, &opt1, NULL, &reply, &error))
|
||||
ogs_error("eir_create_indexes(specific) failed: %s", error.message);
|
||||
bson_destroy(&reply);
|
||||
|
||||
if (!mongoc_collection_create_index_with_opts(
|
||||
collection, keys2, &opt2, NULL, &reply, &error))
|
||||
ogs_error("eir_create_indexes(generic) failed: %s", error.message);
|
||||
bson_destroy(&reply);
|
||||
|
||||
bson_destroy(keys1);
|
||||
bson_destroy(partial1);
|
||||
bson_destroy(keys2);
|
||||
bson_destroy(partial2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Rejects malformed `eir` documents (missing `pei`/`status`, wrong types,
|
||||
* unrecognized `status` values) at write time, on top of the application
|
||||
* level checks in lib/dbi/eir.c and src/eir/n5geir-handler.c.
|
||||
*/
|
||||
static void eir_apply_schema_validation(mongoc_database_t *database)
|
||||
{
|
||||
static const char *validator_json =
|
||||
"{"
|
||||
" \"collMod\": \"eir\","
|
||||
" \"validator\": {"
|
||||
" \"$jsonSchema\": {"
|
||||
" \"bsonType\": \"object\","
|
||||
" \"required\": [\"pei\", \"status\"],"
|
||||
" \"properties\": {"
|
||||
" \"pei\": { \"bsonType\": \"string\" },"
|
||||
" \"supi\": { \"bsonType\": [\"string\", \"null\"] },"
|
||||
" \"status\": {"
|
||||
" \"enum\": "
|
||||
" [\"WHITELISTED\", \"BLACKLISTED\", \"GREYLISTED\"]"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
" },"
|
||||
" \"validationLevel\": \"strict\","
|
||||
" \"validationAction\": \"error\""
|
||||
"}";
|
||||
bson_t *cmd;
|
||||
bson_t reply;
|
||||
bson_error_t error;
|
||||
|
||||
ogs_assert(database);
|
||||
|
||||
cmd = bson_new_from_json((const uint8_t *)validator_json, -1, &error);
|
||||
if (!cmd) {
|
||||
ogs_error("eir_apply_schema_validation() parse failed: %s",
|
||||
error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mongoc_database_write_command_with_opts(
|
||||
database, cmd, NULL, &reply, &error))
|
||||
ogs_error("eir_apply_schema_validation() failed: %s", error.message);
|
||||
|
||||
bson_destroy(&reply);
|
||||
bson_destroy(cmd);
|
||||
}
|
||||
|
||||
int ogs_dbi_init(const char *db_uri)
|
||||
{
|
||||
int rv;
|
||||
|
|
@ -267,10 +173,6 @@ int ogs_dbi_init(const char *db_uri)
|
|||
self.collection.eir = mongoc_client_get_collection(
|
||||
ogs_mongoc()->client, ogs_mongoc()->name, "eir");
|
||||
ogs_assert(self.collection.eir);
|
||||
|
||||
eir_create_indexes(self.collection.eir);
|
||||
eir_apply_schema_validation(
|
||||
(mongoc_database_t *)ogs_mongoc()->database);
|
||||
}
|
||||
|
||||
return OGS_OK;
|
||||
|
|
|
|||
|
|
@ -374,12 +374,16 @@ bool ogs_id_get_type_value(const char *str, char **type, char **value)
|
|||
*type = ogs_id_get_type(str);
|
||||
*value = ogs_id_get_value(str);
|
||||
|
||||
if (!*type || !*value || !strlen(*type) || !strlen(*value))
|
||||
if (!*type || !*value || !strlen(*type) || !strlen(*value)) {
|
||||
ogs_error("Identity type or value is missing");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Reject extra '-' components that ogs_id_get_value() ignores. */
|
||||
if (strlen(str) != strlen(*type) + 1 + strlen(*value))
|
||||
if (strlen(str) != strlen(*type) + 1 + strlen(*value)) {
|
||||
ogs_error("Identity contains extra components");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
|
|
@ -396,22 +400,21 @@ cleanup:
|
|||
return false;
|
||||
}
|
||||
|
||||
bool ogs_bcd_string_is_valid(const char *bcd, int max_len)
|
||||
static bool bcd_string_is_valid(
|
||||
const char *bcd, int min_len, int max_len)
|
||||
{
|
||||
int i, len;
|
||||
size_t i, len = strlen(bcd);
|
||||
|
||||
ogs_assert(bcd);
|
||||
ogs_assert(max_len > 0);
|
||||
|
||||
len = strlen(bcd);
|
||||
if (len == 0 || len > max_len) {
|
||||
ogs_error("Invalid BCD length [%d:%s]", len, bcd);
|
||||
if (len < (size_t)min_len || len > (size_t)max_len) {
|
||||
ogs_warn("Invalid BCD string length [%zu], expected [%d..%d]",
|
||||
len, min_len, max_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
if (bcd[i] < '0' || bcd[i] > '9') {
|
||||
ogs_error("Invalid BCD digit [%d:%c:%s]", i, bcd[i], bcd);
|
||||
ogs_warn("Invalid BCD character [0x%02x] at offset [%zu]",
|
||||
(unsigned char)bcd[i], i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -419,6 +422,37 @@ bool ogs_bcd_string_is_valid(const char *bcd, int max_len)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool ogs_bcd_string_is_valid(const char *bcd, int max_len)
|
||||
{
|
||||
ogs_assert(bcd);
|
||||
ogs_assert(max_len > 0);
|
||||
|
||||
return bcd_string_is_valid(bcd, 1, max_len);
|
||||
}
|
||||
|
||||
bool ogs_id_bcd_is_valid(
|
||||
const char *str, const char *type, int min_len, int max_len)
|
||||
{
|
||||
size_t type_len;
|
||||
|
||||
ogs_assert(type && type[0]);
|
||||
ogs_assert(min_len > 0);
|
||||
ogs_assert(max_len >= min_len);
|
||||
|
||||
if (!str) {
|
||||
ogs_warn("Missing identity, expected type [%s]", type);
|
||||
return false;
|
||||
}
|
||||
|
||||
type_len = strlen(type);
|
||||
if (strncmp(str, type, type_len) || str[type_len] != '-') {
|
||||
ogs_warn("Invalid identity prefix, expected [%s-]", type);
|
||||
return false;
|
||||
}
|
||||
|
||||
return bcd_string_is_valid(str + type_len + 1, min_len, max_len);
|
||||
}
|
||||
|
||||
bool ogs_pdu_session_id_is_valid(int psi)
|
||||
{
|
||||
return psi > OGS_NAS_PDU_SESSION_IDENTITY_UNASSIGNED &&
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ extern "C" {
|
|||
#define OGS_MAX_IMSI_LEN \
|
||||
OGS_BCD_TO_BUFFER_LEN(OGS_MAX_IMSI_BCD_LEN)
|
||||
|
||||
#define OGS_MAX_IMEI_BCD_LEN 15
|
||||
#define OGS_MAX_IMEISV_BCD_LEN 16
|
||||
#define OGS_MAX_IMEISV_LEN \
|
||||
OGS_BCD_TO_BUFFER_LEN(OGS_MAX_IMEISV_BCD_LEN)
|
||||
|
|
@ -308,11 +309,21 @@ ogs_amf_id_t *ogs_amf_id_build(ogs_amf_id_t *amf_id,
|
|||
#define OGS_ID_SUPI_TYPE_IMSI "imsi"
|
||||
#define OGS_ID_GPSI_TYPE_MSISDN "msisdn"
|
||||
#define OGS_ID_SUPI_TYPE_IMEISV "imeisv"
|
||||
#define OGS_ID_PEI_TYPE_IMEI "imei"
|
||||
#define OGS_ID_5G_GUTI_TYPE "5g-guti"
|
||||
char *ogs_id_get_type(const char *str);
|
||||
char *ogs_id_get_value(const char *str);
|
||||
bool ogs_id_get_type_value(const char *str, char **type, char **value);
|
||||
/* Numeric validation logs the reason for invalid input at WARNING level. */
|
||||
bool ogs_bcd_string_is_valid(const char *bcd, int max_len);
|
||||
/*
|
||||
* Check "<type>-<decimal digits>" without allocation.
|
||||
* NULL or malformed input logs a warning and returns false.
|
||||
* Bounds apply to the digit count;
|
||||
* the caller chooses the supported type and positive minimum/maximum lengths.
|
||||
*/
|
||||
bool ogs_id_bcd_is_valid(
|
||||
const char *str, const char *type, int min_len, int max_len);
|
||||
bool ogs_imsi_bcd_is_valid(const char *imsi_bcd);
|
||||
bool ogs_imeisv_bcd_is_valid(const char *imeisv_bcd);
|
||||
int ogs_supi_to_imsi_bcd(
|
||||
|
|
|
|||
|
|
@ -44,16 +44,6 @@ void gmm_state_initial_context_setup(ogs_fsm_t *s, amf_event_t *e);
|
|||
void gmm_state_registered(ogs_fsm_t *s, amf_event_t *e);
|
||||
void gmm_state_ue_context_will_remove(ogs_fsm_t *s, amf_event_t *e);
|
||||
void gmm_state_exception(ogs_fsm_t *s, amf_event_t *e);
|
||||
void gmm_state_equipment_identity_check(ogs_fsm_t *s, amf_event_t *e);
|
||||
|
||||
typedef enum {
|
||||
AMF_EIR_RESULT_ALLOW = 0,
|
||||
AMF_EIR_RESULT_BLACKLISTED,
|
||||
AMF_EIR_RESULT_REJECT,
|
||||
} amf_eir_result_e;
|
||||
|
||||
void gmm_ue_equipment_check_complete(
|
||||
amf_ue_t *amf_ue, amf_eir_result_e result);
|
||||
|
||||
#define amf_sm_debug(__pe) \
|
||||
ogs_debug("%s(): %s", __func__, amf_event_get_name(__pe))
|
||||
|
|
|
|||
|
|
@ -129,6 +129,23 @@ static int amf_context_prepare(void)
|
|||
return OGS_OK;
|
||||
}
|
||||
|
||||
static int amf_context_parse_eir_action(
|
||||
ogs_yaml_iter_t *iter, const char *key, amf_eir_action_e *action)
|
||||
{
|
||||
const char *value = ogs_yaml_iter_value(iter);
|
||||
|
||||
if (!value || !strcmp(value, "allow"))
|
||||
*action = AMF_EIR_ACTION_ALLOW;
|
||||
else if (!strcmp(value, "reject"))
|
||||
*action = AMF_EIR_ACTION_REJECT;
|
||||
else {
|
||||
ogs_error("invalid %s `%s` (expected `allow` or `reject`)", key, value);
|
||||
return OGS_ERROR;
|
||||
}
|
||||
|
||||
return OGS_OK;
|
||||
}
|
||||
|
||||
static int amf_context_validation(void)
|
||||
{
|
||||
ogs_nas_gprs_timer_t gprs_timer;
|
||||
|
|
@ -1075,43 +1092,28 @@ int amf_context_parse_config(void)
|
|||
self.eir.enabled =
|
||||
ogs_yaml_iter_bool(&eir_iter);
|
||||
} else if (!strcmp(eir_key, "unknown_action")) {
|
||||
const char *v = ogs_yaml_iter_value(&eir_iter);
|
||||
if (v && !strcmp(v, "reject"))
|
||||
self.eir.unknown_action =
|
||||
AMF_EIR_ACTION_REJECT;
|
||||
else if (!v || !strcmp(v, "allow"))
|
||||
self.eir.unknown_action =
|
||||
AMF_EIR_ACTION_ALLOW;
|
||||
else {
|
||||
ogs_error("invalid unknown_action `%s` "
|
||||
"(expected `allow` or `reject`)", v);
|
||||
return OGS_ERROR;
|
||||
rv = amf_context_parse_eir_action(&eir_iter,
|
||||
eir_key, &self.eir.unknown_action);
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("amf_context_parse_eir_action"
|
||||
"(unknown_action) failed");
|
||||
return rv;
|
||||
}
|
||||
} else if (!strcmp(eir_key, "failure_action")) {
|
||||
const char *v = ogs_yaml_iter_value(&eir_iter);
|
||||
if (v && !strcmp(v, "reject"))
|
||||
self.eir.failure_action =
|
||||
AMF_EIR_ACTION_REJECT;
|
||||
else if (!v || !strcmp(v, "allow"))
|
||||
self.eir.failure_action =
|
||||
AMF_EIR_ACTION_ALLOW;
|
||||
else {
|
||||
ogs_error("invalid failure_action `%s` "
|
||||
"(expected `allow` or `reject`)", v);
|
||||
return OGS_ERROR;
|
||||
rv = amf_context_parse_eir_action(&eir_iter,
|
||||
eir_key, &self.eir.failure_action);
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("amf_context_parse_eir_action"
|
||||
"(failure_action) failed");
|
||||
return rv;
|
||||
}
|
||||
} else if (!strcmp(eir_key, "missing_pei_action")) {
|
||||
const char *v = ogs_yaml_iter_value(&eir_iter);
|
||||
if (v && !strcmp(v, "reject"))
|
||||
self.eir.missing_pei_action =
|
||||
AMF_EIR_ACTION_REJECT;
|
||||
else if (!v || !strcmp(v, "allow"))
|
||||
self.eir.missing_pei_action =
|
||||
AMF_EIR_ACTION_ALLOW;
|
||||
else {
|
||||
ogs_error("invalid missing_pei_action `%s` "
|
||||
"(expected `allow` or `reject`)", v);
|
||||
return OGS_ERROR;
|
||||
rv = amf_context_parse_eir_action(&eir_iter,
|
||||
eir_key, &self.eir.missing_pei_action);
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("amf_context_parse_eir_action"
|
||||
"(missing_pei_action) failed");
|
||||
return rv;
|
||||
}
|
||||
} else
|
||||
ogs_warn("unknown key `%s`", eir_key);
|
||||
|
|
|
|||
|
|
@ -387,6 +387,7 @@ struct amf_ue_s {
|
|||
ogs_plmn_id_t home_plmn_id;
|
||||
|
||||
char *pei;
|
||||
bool eir_check_pending;
|
||||
uint8_t masked_imeisv[OGS_MAX_IMEISV_LEN];
|
||||
int masked_imeisv_len;
|
||||
char imeisv_bcd[OGS_MAX_IMEISV_BCD_LEN+1];
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ const char *amf_event_get_name(amf_event_t *e)
|
|||
return "AMF_EVENT_5GMM_MESSAGE";
|
||||
case AMF_EVENT_5GMM_TIMER:
|
||||
return "AMF_EVENT_5GMM_TIMER";
|
||||
case AMF_EVENT_5GMM_EIR_FAILURE:
|
||||
return "AMF_EVENT_5GMM_EIR_FAILURE";
|
||||
case AMF_EVENT_5GSM_MESSAGE:
|
||||
return "AMF_EVENT_5GSM_MESSAGE";
|
||||
case AMF_EVENT_5GSM_TIMER:
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ typedef enum {
|
|||
|
||||
AMF_EVENT_5GMM_MESSAGE,
|
||||
AMF_EVENT_5GMM_TIMER,
|
||||
AMF_EVENT_5GMM_EIR_FAILURE,
|
||||
AMF_EVENT_5GSM_MESSAGE,
|
||||
AMF_EVENT_5GSM_TIMER,
|
||||
|
||||
|
|
|
|||
246
src/amf/gmm-sm.c
246
src/amf/gmm-sm.c
|
|
@ -28,7 +28,6 @@
|
|||
#include "nudm-handler.h"
|
||||
#include "npcf-handler.h"
|
||||
#include "namf-handler.h"
|
||||
#include "n5geir-build.h"
|
||||
#include "n5geir-handler.h"
|
||||
#include "sbi-path.h"
|
||||
#include "amf-sm.h"
|
||||
|
|
@ -2042,7 +2041,7 @@ static void common_register_state(ogs_fsm_t *s, amf_event_t *e,
|
|||
break;
|
||||
|
||||
case OGS_NAS_5GS_CONFIGURATION_UPDATE_COMPLETE:
|
||||
ogs_debug("[%s] Configuration update complete", amf_ue->supi);
|
||||
ogs_info("[%s] Configuration update complete", amf_ue->supi);
|
||||
|
||||
amf_metrics_inst_global_inc(AMF_METR_GLOB_CTR_MM_CONF_UPDATE_SUCC);
|
||||
|
||||
|
|
@ -2635,105 +2634,79 @@ static void gmm_continue_registration(ogs_fsm_t *s, amf_ue_t *amf_ue)
|
|||
}
|
||||
}
|
||||
|
||||
void gmm_ue_equipment_check_complete(
|
||||
amf_ue_t *amf_ue, amf_eir_result_e result)
|
||||
static void gmm_complete_equipment_identity_check(
|
||||
ogs_fsm_t *s, amf_ue_t *amf_ue, ogs_nas_5gmm_cause_t cause)
|
||||
{
|
||||
int r;
|
||||
ran_ue_t *ran_ue = NULL;
|
||||
|
||||
ogs_assert(s);
|
||||
ogs_assert(amf_ue);
|
||||
|
||||
switch (result) {
|
||||
case AMF_EIR_RESULT_ALLOW:
|
||||
gmm_continue_registration(&amf_ue->sm, amf_ue);
|
||||
ran_ue = ran_ue_find_by_id(amf_ue->ran_ue_id);
|
||||
if (!ran_ue) {
|
||||
ogs_error("[%s] NG context has already been removed", amf_ue->supi);
|
||||
OGS_FSM_TRAN(s, &gmm_state_exception);
|
||||
return;
|
||||
|
||||
case AMF_EIR_RESULT_BLACKLISTED:
|
||||
ran_ue = ran_ue_find_by_id(amf_ue->ran_ue_id);
|
||||
if (!ran_ue) {
|
||||
ogs_error("[%s] NG context has already been removed",
|
||||
amf_ue->supi);
|
||||
return;
|
||||
}
|
||||
ogs_warn("[%s] Equipment blacklisted by 5G-EIR", amf_ue->supi);
|
||||
r = nas_5gs_send_registration_reject(
|
||||
ran_ue, amf_ue, OGS_5GMM_CAUSE_ILLEGAL_ME);
|
||||
ogs_expect(r == OGS_OK);
|
||||
ogs_assert(r != OGS_ERROR);
|
||||
OGS_FSM_TRAN(&amf_ue->sm, &gmm_state_exception);
|
||||
return;
|
||||
|
||||
case AMF_EIR_RESULT_REJECT:
|
||||
ran_ue = ran_ue_find_by_id(amf_ue->ran_ue_id);
|
||||
if (!ran_ue) {
|
||||
ogs_error("[%s] NG context has already been removed",
|
||||
amf_ue->supi);
|
||||
return;
|
||||
}
|
||||
r = nas_5gs_send_registration_reject(
|
||||
ran_ue, amf_ue, OGS_5GMM_CAUSE_5GS_SERVICES_NOT_ALLOWED);
|
||||
ogs_expect(r == OGS_OK);
|
||||
ogs_assert(r != OGS_ERROR);
|
||||
OGS_FSM_TRAN(&amf_ue->sm, &gmm_state_exception);
|
||||
return;
|
||||
|
||||
default:
|
||||
ogs_error("Unknown EIR result[%d]", result);
|
||||
ogs_assert_if_reached();
|
||||
}
|
||||
|
||||
if (cause == OGS_5GMM_CAUSE_REQUEST_ACCEPTED) {
|
||||
ogs_info("[%s] Continue registration after 5G-EIR check",
|
||||
amf_ue->supi);
|
||||
gmm_continue_registration(s, amf_ue);
|
||||
return;
|
||||
}
|
||||
|
||||
ogs_warn("[%s] Registration rejected after 5G-EIR check [cause:%d]",
|
||||
amf_ue->supi, cause);
|
||||
r = nas_5gs_send_registration_reject(ran_ue, amf_ue, cause);
|
||||
ogs_expect(r == OGS_OK);
|
||||
ogs_assert(r != OGS_ERROR);
|
||||
OGS_FSM_TRAN(s, &gmm_state_exception);
|
||||
}
|
||||
|
||||
void gmm_state_equipment_identity_check(ogs_fsm_t *s, amf_event_t *e)
|
||||
static void gmm_security_mode_completed(ogs_fsm_t *s, amf_ue_t *amf_ue)
|
||||
{
|
||||
amf_ue_t *amf_ue = NULL;
|
||||
ogs_sbi_message_t *sbi_message = NULL;
|
||||
int service_name_id = OpenAPI_service_name_NULL;
|
||||
int r;
|
||||
ogs_nas_5gmm_cause_t cause;
|
||||
|
||||
ogs_assert(s);
|
||||
ogs_assert(e);
|
||||
|
||||
amf_sm_debug(e);
|
||||
|
||||
amf_ue = amf_ue_find_by_id(e->amf_ue_id);
|
||||
ogs_assert(amf_ue);
|
||||
|
||||
switch (e->h.id) {
|
||||
case OGS_FSM_ENTRY_SIG:
|
||||
break;
|
||||
|
||||
case OGS_FSM_EXIT_SIG:
|
||||
break;
|
||||
|
||||
case OGS_EVENT_SBI_CLIENT:
|
||||
sbi_message = e->h.sbi.message;
|
||||
ogs_assert(sbi_message);
|
||||
|
||||
service_name_id = ogs_sbi_service_name_id_from_string(
|
||||
sbi_message->h.service.name);
|
||||
switch (service_name_id) {
|
||||
case OpenAPI_service_name_n5g_eir_eic:
|
||||
SWITCH(sbi_message->h.resource.component[0])
|
||||
CASE(OGS_SBI_RESOURCE_NAME_EQUIPMENT_STATUS)
|
||||
amf_n5geir_eic_handle_equipment_status(amf_ue, sbi_message);
|
||||
break;
|
||||
|
||||
DEFAULT
|
||||
ogs_error("Invalid resource name [%s]",
|
||||
sbi_message->h.resource.component[0]);
|
||||
ogs_assert_if_reached();
|
||||
END
|
||||
break;
|
||||
|
||||
default:
|
||||
ogs_error("Invalid API name [%s]", sbi_message->h.service.name);
|
||||
ogs_assert_if_reached();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ogs_error("No handler for event %s", amf_event_get_name(e));
|
||||
break;
|
||||
if (amf_ue->eir_check_pending) {
|
||||
ogs_error("[%s] 5G-EIR check is already pending", amf_ue->supi);
|
||||
return;
|
||||
}
|
||||
|
||||
if (amf_ue->nas.message_type != OGS_NAS_5GS_REGISTRATION_REQUEST ||
|
||||
!amf_self()->eir.enabled) {
|
||||
ogs_info("[%s] Skip 5G-EIR check [message:%d,enabled:%d]",
|
||||
amf_ue->supi, amf_ue->nas.message_type,
|
||||
amf_self()->eir.enabled);
|
||||
gmm_continue_registration(s, amf_ue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (amf_ue->pei) {
|
||||
r = amf_ue_sbi_discover_and_send_eir(amf_ue);
|
||||
if (r == OGS_OK) {
|
||||
amf_ue->eir_check_pending = true;
|
||||
ogs_info("[%s] Waiting for 5G-EIR equipment status", amf_ue->supi);
|
||||
return;
|
||||
}
|
||||
|
||||
cause = amf_n5geir_eic_failure_cause();
|
||||
} else {
|
||||
ogs_error("[%s] No PEI available for 5G-EIR "
|
||||
"[missing_pei_action:%s]", amf_ue->supi,
|
||||
amf_self()->eir.missing_pei_action ==
|
||||
AMF_EIR_ACTION_REJECT ? "reject" : "allow");
|
||||
cause = amf_self()->eir.missing_pei_action == AMF_EIR_ACTION_REJECT ?
|
||||
OGS_5GMM_CAUSE_5GS_SERVICES_NOT_ALLOWED :
|
||||
OGS_5GMM_CAUSE_REQUEST_ACCEPTED;
|
||||
}
|
||||
|
||||
gmm_complete_equipment_identity_check(s, amf_ue, cause);
|
||||
}
|
||||
|
||||
void gmm_state_security_mode(ogs_fsm_t *s, amf_event_t *e)
|
||||
|
|
@ -2745,6 +2718,7 @@ void gmm_state_security_mode(ogs_fsm_t *s, amf_event_t *e)
|
|||
ogs_nas_5gs_message_t *nas_message = NULL;
|
||||
ogs_nas_security_header_type_t h;
|
||||
ogs_sbi_message_t *sbi_message = NULL;
|
||||
ogs_sbi_xact_t *xact = NULL, *next_xact = NULL;
|
||||
int service_name_id = OpenAPI_service_name_NULL;
|
||||
|
||||
ogs_assert(s);
|
||||
|
|
@ -2763,6 +2737,24 @@ void gmm_state_security_mode(ogs_fsm_t *s, amf_event_t *e)
|
|||
ogs_assert(r != OGS_ERROR);
|
||||
break;
|
||||
case OGS_FSM_EXIT_SIG:
|
||||
/* A result belongs only to this registration procedure. */
|
||||
amf_ue->eir_check_pending = false;
|
||||
ogs_list_for_each_safe(&amf_ue->sbi.xact_list, next_xact, xact) {
|
||||
if (xact->service_name == OpenAPI_service_name_n5g_eir_eic) {
|
||||
ogs_error("[%s] Cancel pending 5G-EIR transaction",
|
||||
amf_ue->supi);
|
||||
ogs_sbi_xact_remove(xact);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AMF_EVENT_5GMM_EIR_FAILURE:
|
||||
if (!amf_ue->eir_check_pending) {
|
||||
ogs_error("[%s] Ignore stale 5G-EIR failure", amf_ue->supi);
|
||||
break;
|
||||
}
|
||||
|
||||
gmm_cause = amf_n5geir_eic_failure_cause();
|
||||
gmm_complete_equipment_identity_check(s, amf_ue, gmm_cause);
|
||||
break;
|
||||
case AMF_EVENT_5GMM_MESSAGE:
|
||||
nas_message = e->nas.message;
|
||||
|
|
@ -2783,7 +2775,13 @@ void gmm_state_security_mode(ogs_fsm_t *s, amf_event_t *e)
|
|||
|
||||
switch (nas_message->gmm.h.message_type) {
|
||||
case OGS_NAS_5GS_SECURITY_MODE_COMPLETE:
|
||||
ogs_debug("[%s] Security mode complete", amf_ue->supi);
|
||||
if (amf_ue->eir_check_pending) {
|
||||
ogs_error("[%s] Ignore repeated Security mode complete",
|
||||
amf_ue->supi);
|
||||
break;
|
||||
}
|
||||
|
||||
ogs_info("[%s] Security mode complete", amf_ue->supi);
|
||||
|
||||
/*
|
||||
* TS24.501
|
||||
|
|
@ -2874,34 +2872,7 @@ void gmm_state_security_mode(ogs_fsm_t *s, amf_event_t *e)
|
|||
ogs_kdf_nh_gnb(amf_ue->kamf, amf_ue->kgnb, amf_ue->nh);
|
||||
amf_ue->nhcc = 1;
|
||||
|
||||
if (amf_ue->nas.message_type == OGS_NAS_5GS_REGISTRATION_REQUEST &&
|
||||
amf_self()->eir.enabled && amf_ue->pei) {
|
||||
r = amf_ue_sbi_discover_and_send(
|
||||
OpenAPI_service_name_n5g_eir_eic, NULL,
|
||||
amf_n5geir_eic_build_equipment_status,
|
||||
amf_ue, 0, NULL);
|
||||
ogs_expect(r == OGS_OK);
|
||||
ogs_assert(r != OGS_ERROR);
|
||||
|
||||
OGS_FSM_TRAN(s, &gmm_state_equipment_identity_check);
|
||||
break;
|
||||
}
|
||||
|
||||
if (amf_ue->nas.message_type == OGS_NAS_5GS_REGISTRATION_REQUEST &&
|
||||
amf_self()->eir.enabled && !amf_ue->pei) {
|
||||
if (amf_self()->eir.missing_pei_action ==
|
||||
AMF_EIR_ACTION_REJECT) {
|
||||
ogs_warn("[%s] No PEI available, rejecting per "
|
||||
"missing_pei_action policy", amf_ue->supi);
|
||||
gmm_ue_equipment_check_complete(
|
||||
amf_ue, AMF_EIR_RESULT_REJECT);
|
||||
break;
|
||||
}
|
||||
ogs_warn("[%s] No PEI available, allowing per "
|
||||
"missing_pei_action policy", amf_ue->supi);
|
||||
}
|
||||
|
||||
gmm_continue_registration(s, amf_ue);
|
||||
gmm_security_mode_completed(s, amf_ue);
|
||||
break;
|
||||
case OGS_NAS_5GS_SECURITY_MODE_REJECT:
|
||||
ogs_warn("[%s] Security mode reject : Cause[%d]",
|
||||
|
|
@ -3045,23 +3016,7 @@ void gmm_state_security_mode(ogs_fsm_t *s, amf_event_t *e)
|
|||
ogs_kdf_nh_gnb(amf_ue->kamf, amf_ue->kgnb, amf_ue->nh);
|
||||
amf_ue->nhcc = 1;
|
||||
|
||||
r = amf_ue_sbi_discover_and_send(
|
||||
OpenAPI_service_name_nudm_uecm, NULL,
|
||||
amf_nudm_uecm_build_registration, amf_ue, 0, NULL);
|
||||
ogs_expect(r == OGS_OK);
|
||||
ogs_assert(r != OGS_ERROR);
|
||||
|
||||
if (amf_ue->nas.message_type ==
|
||||
OGS_NAS_5GS_REGISTRATION_REQUEST) {
|
||||
OGS_FSM_TRAN(s, &gmm_state_initial_context_setup);
|
||||
} else if (amf_ue->nas.message_type ==
|
||||
OGS_NAS_5GS_SERVICE_REQUEST) {
|
||||
OGS_FSM_TRAN(s, &gmm_state_registered);
|
||||
} else {
|
||||
ogs_fatal("Invalid OGS_NAS_5GS[%d]",
|
||||
amf_ue->nas.message_type);
|
||||
ogs_assert_if_reached();
|
||||
}
|
||||
gmm_security_mode_completed(s, amf_ue);
|
||||
break;
|
||||
|
||||
DEFAULT
|
||||
|
|
@ -3078,6 +3033,27 @@ void gmm_state_security_mode(ogs_fsm_t *s, amf_event_t *e)
|
|||
END
|
||||
break;
|
||||
|
||||
case OpenAPI_service_name_n5g_eir_eic:
|
||||
if (!amf_ue->eir_check_pending) {
|
||||
ogs_error("[%s] Ignore unexpected 5G-EIR response",
|
||||
amf_ue->supi);
|
||||
break;
|
||||
}
|
||||
|
||||
SWITCH(sbi_message->h.resource.component[0])
|
||||
CASE(OGS_SBI_RESOURCE_NAME_EQUIPMENT_STATUS)
|
||||
gmm_cause = amf_n5geir_eic_handle_equipment_status(
|
||||
amf_ue, sbi_message);
|
||||
gmm_complete_equipment_identity_check(s, amf_ue, gmm_cause);
|
||||
break;
|
||||
|
||||
DEFAULT
|
||||
ogs_error("Invalid resource name [%s]",
|
||||
sbi_message->h.resource.component[0]);
|
||||
ogs_assert_if_reached();
|
||||
END
|
||||
break;
|
||||
|
||||
default:
|
||||
ogs_error("Invalid service name [%s]", sbi_message->h.service.name);
|
||||
ogs_assert_if_reached();
|
||||
|
|
@ -3087,6 +3063,14 @@ void gmm_state_security_mode(ogs_fsm_t *s, amf_event_t *e)
|
|||
case AMF_EVENT_5GMM_TIMER:
|
||||
switch (e->h.timer_id) {
|
||||
case AMF_TIMER_T3560:
|
||||
/* Security Mode Complete clears the saved command. Ignore a
|
||||
* queued expiry while waiting for UE context transfer or EIR. */
|
||||
if (!amf_ue->t3560.pkbuf) {
|
||||
ogs_error("[%s] Ignore T3560 expiry after command cleared",
|
||||
amf_ue->supi);
|
||||
break;
|
||||
}
|
||||
|
||||
if (amf_ue->t3560.retry_count >=
|
||||
amf_timer_cfg(AMF_TIMER_T3560)->max_count) {
|
||||
ogs_warn("[%s] Retransmission failed. Stop", amf_ue->supi);
|
||||
|
|
|
|||
|
|
@ -20,73 +20,91 @@
|
|||
#include "n5geir-handler.h"
|
||||
#include "amf-sm.h"
|
||||
|
||||
static amf_eir_result_e failure_result(void)
|
||||
/* The 5G-EIR gave no usable answer: reject as for any unreachable NF */
|
||||
ogs_nas_5gmm_cause_t amf_n5geir_eic_failure_cause(void)
|
||||
{
|
||||
ogs_warn("Applying 5G-EIR failure_action[%s]",
|
||||
amf_self()->eir.failure_action ==
|
||||
AMF_EIR_ACTION_REJECT ? "reject" : "allow");
|
||||
return amf_self()->eir.failure_action == AMF_EIR_ACTION_REJECT ?
|
||||
AMF_EIR_RESULT_REJECT : AMF_EIR_RESULT_ALLOW;
|
||||
OGS_5GMM_CAUSE_PAYLOAD_WAS_NOT_FORWARDED :
|
||||
OGS_5GMM_CAUSE_REQUEST_ACCEPTED;
|
||||
}
|
||||
|
||||
void amf_n5geir_eic_handle_equipment_status(
|
||||
ogs_nas_5gmm_cause_t amf_n5geir_eic_handle_equipment_status(
|
||||
amf_ue_t *amf_ue, ogs_sbi_message_t *recvmsg)
|
||||
{
|
||||
OpenAPI_eir_response_data_t *EirResponseData = NULL;
|
||||
|
||||
ogs_assert(amf_ue);
|
||||
ogs_assert(recvmsg);
|
||||
|
||||
switch (recvmsg->res_status) {
|
||||
case OGS_SBI_HTTP_STATUS_OK:
|
||||
if (!recvmsg->EirResponseData) {
|
||||
ogs_error("[%s] No EirResponseData", amf_ue->supi);
|
||||
gmm_ue_equipment_check_complete(amf_ue, failure_result());
|
||||
return;
|
||||
}
|
||||
if (recvmsg->res_status == OGS_SBI_HTTP_STATUS_NOT_FOUND &&
|
||||
recvmsg->ProblemDetails && recvmsg->ProblemDetails->cause &&
|
||||
!strcmp(recvmsg->ProblemDetails->cause, "ERROR_EQUIPMENT_UNKNOWN")) {
|
||||
ogs_info("[%s] Unknown equipment [%s] [unknown_action:%s]",
|
||||
amf_ue->supi, amf_ue->pei,
|
||||
amf_self()->eir.unknown_action ==
|
||||
AMF_EIR_ACTION_REJECT ? "reject" : "allow");
|
||||
return amf_self()->eir.unknown_action == AMF_EIR_ACTION_REJECT ?
|
||||
OGS_5GMM_CAUSE_5GS_SERVICES_NOT_ALLOWED :
|
||||
OGS_5GMM_CAUSE_REQUEST_ACCEPTED;
|
||||
}
|
||||
|
||||
switch (recvmsg->EirResponseData->status) {
|
||||
case OpenAPI_equipment_status_WHITELISTED:
|
||||
gmm_ue_equipment_check_complete(amf_ue, AMF_EIR_RESULT_ALLOW);
|
||||
break;
|
||||
if (recvmsg->res_status != OGS_SBI_HTTP_STATUS_OK) {
|
||||
ogs_warn("[%s] 5G-EIR HTTP response error [%d]",
|
||||
amf_ue->supi, recvmsg->res_status);
|
||||
return amf_n5geir_eic_failure_cause();
|
||||
}
|
||||
|
||||
case OpenAPI_equipment_status_GREYLISTED:
|
||||
ogs_warn("[%s] Equipment greylisted by 5G-EIR", amf_ue->supi);
|
||||
gmm_ue_equipment_check_complete(amf_ue, AMF_EIR_RESULT_ALLOW);
|
||||
break;
|
||||
EirResponseData = recvmsg->EirResponseData;
|
||||
if (!EirResponseData) {
|
||||
ogs_error("[%s] No EirResponseData", amf_ue->supi);
|
||||
return amf_n5geir_eic_failure_cause();
|
||||
}
|
||||
|
||||
case OpenAPI_equipment_status_BLACKLISTED:
|
||||
gmm_ue_equipment_check_complete(
|
||||
amf_ue, AMF_EIR_RESULT_BLACKLISTED);
|
||||
break;
|
||||
switch (EirResponseData->status) {
|
||||
case OpenAPI_equipment_status_WHITELISTED:
|
||||
ogs_info("[%s] Whitelisted equipment [%s]",
|
||||
amf_ue->supi, amf_ue->pei);
|
||||
return OGS_5GMM_CAUSE_REQUEST_ACCEPTED;
|
||||
|
||||
default:
|
||||
ogs_error("[%s] Malformed EirResponseData", amf_ue->supi);
|
||||
gmm_ue_equipment_check_complete(amf_ue, failure_result());
|
||||
}
|
||||
break;
|
||||
case OpenAPI_equipment_status_GREYLISTED:
|
||||
ogs_warn("[%s] Greylisted equipment [%s]", amf_ue->supi, amf_ue->pei);
|
||||
return OGS_5GMM_CAUSE_REQUEST_ACCEPTED;
|
||||
|
||||
case OGS_SBI_HTTP_STATUS_NOT_FOUND:
|
||||
if (recvmsg->ProblemDetails && recvmsg->ProblemDetails->cause &&
|
||||
!strcmp(recvmsg->ProblemDetails->cause,
|
||||
"ERROR_EQUIPMENT_UNKNOWN")) {
|
||||
ogs_info("[%s] Unknown equipment (5G-EIR)", amf_ue->supi);
|
||||
gmm_ue_equipment_check_complete(amf_ue,
|
||||
amf_self()->eir.unknown_action == AMF_EIR_ACTION_REJECT ?
|
||||
AMF_EIR_RESULT_REJECT : AMF_EIR_RESULT_ALLOW);
|
||||
} else {
|
||||
ogs_error("[%s] 5G-EIR 404 without "
|
||||
"ERROR_EQUIPMENT_UNKNOWN cause", amf_ue->supi);
|
||||
gmm_ue_equipment_check_complete(amf_ue, failure_result());
|
||||
}
|
||||
break;
|
||||
case OpenAPI_equipment_status_BLACKLISTED:
|
||||
ogs_warn("[%s] Blacklisted equipment [%s]",
|
||||
amf_ue->supi, amf_ue->pei);
|
||||
return OGS_5GMM_CAUSE_ILLEGAL_ME;
|
||||
|
||||
default:
|
||||
ogs_error("[%s] 5G-EIR HTTP response error [%d]",
|
||||
amf_ue->supi, recvmsg->res_status);
|
||||
gmm_ue_equipment_check_complete(amf_ue, failure_result());
|
||||
ogs_error("[%s] Unknown EquipmentStatus [%d]",
|
||||
amf_ue->supi, EirResponseData->status);
|
||||
return amf_n5geir_eic_failure_cause();
|
||||
}
|
||||
}
|
||||
|
||||
void amf_n5geir_eic_handle_discovery_failure(amf_ue_t *amf_ue)
|
||||
{
|
||||
amf_event_t e;
|
||||
|
||||
ogs_assert(amf_ue);
|
||||
|
||||
if (!OGS_FSM_CHECK(&amf_ue->sm, gmm_state_security_mode) ||
|
||||
!amf_ue->eir_check_pending) {
|
||||
ogs_info("[%s] Ignore stale 5G-EIR discovery/communication failure",
|
||||
amf_ue->supi);
|
||||
return;
|
||||
}
|
||||
|
||||
ogs_error("[%s] 5G-EIR discovery/communication failure", amf_ue->supi);
|
||||
gmm_ue_equipment_check_complete(amf_ue, failure_result());
|
||||
|
||||
/* Discovery failures and timeouts arrive outside UE FSM dispatch.
|
||||
* Dispatch locally so leaving security mode cancels its EIR transactions
|
||||
* and runs the next state's entry actions. */
|
||||
memset(&e, 0, sizeof(e));
|
||||
e.h.id = AMF_EVENT_5GMM_EIR_FAILURE;
|
||||
e.amf_ue_id = amf_ue->id;
|
||||
ogs_fsm_dispatch(&amf_ue->sm, &e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
void amf_n5geir_eic_handle_equipment_status(
|
||||
ogs_nas_5gmm_cause_t amf_n5geir_eic_failure_cause(void);
|
||||
|
||||
ogs_nas_5gmm_cause_t amf_n5geir_eic_handle_equipment_status(
|
||||
amf_ue_t *amf_ue, ogs_sbi_message_t *recvmsg);
|
||||
void amf_n5geir_eic_handle_discovery_failure(amf_ue_t *amf_ue);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include "nas-path.h"
|
||||
#include "ngap-path.h"
|
||||
#include "nnrf-handler.h"
|
||||
#include "n5geir-build.h"
|
||||
|
||||
int amf_sbi_open(void)
|
||||
{
|
||||
|
|
@ -158,6 +159,33 @@ int amf_ue_sbi_discover_and_send(
|
|||
return OGS_OK;
|
||||
}
|
||||
|
||||
/* The UE FSM applies failure_action instead of the generic SBI reject. */
|
||||
int amf_ue_sbi_discover_and_send_eir(amf_ue_t *amf_ue)
|
||||
{
|
||||
int rv;
|
||||
ogs_sbi_xact_t *xact = NULL;
|
||||
|
||||
ogs_assert(amf_ue);
|
||||
|
||||
xact = ogs_sbi_xact_add(
|
||||
amf_ue->id, &amf_ue->sbi,
|
||||
OpenAPI_service_name_n5g_eir_eic, NULL,
|
||||
(ogs_sbi_build_f)amf_n5geir_eic_build_equipment_status,
|
||||
amf_ue, NULL);
|
||||
if (!xact) {
|
||||
ogs_error("[%s] Cannot create 5G-EIR transaction", amf_ue->supi);
|
||||
return OGS_ERROR;
|
||||
}
|
||||
|
||||
rv = ogs_sbi_discover_and_send(xact);
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("[%s] Cannot send 5G-EIR request [error:%d]",
|
||||
amf_ue->supi, rv);
|
||||
ogs_sbi_xact_remove(xact);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void amf_sbi_xact_ctx_free(void *data)
|
||||
{
|
||||
ogs_free(data);
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ int amf_ue_sbi_discover_and_send(
|
|||
ogs_sbi_discovery_option_t *discovery_option,
|
||||
ogs_sbi_request_t *(*build)(amf_ue_t *amf_ue, void *data),
|
||||
amf_ue_t *amf_ue, int state, void *data);
|
||||
int amf_ue_sbi_discover_and_send_eir(amf_ue_t *amf_ue);
|
||||
int amf_sess_sbi_discover_and_send(
|
||||
OpenAPI_service_name_e service_name,
|
||||
ogs_sbi_discovery_option_t *discovery_option,
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ void eir_state_operational(ogs_fsm_t *s, eir_event_t *e)
|
|||
}
|
||||
|
||||
if (strcmp(message.h.api.version, OGS_SBI_API_V1) != 0) {
|
||||
ogs_error("Not supported version [%s]", message.h.api.version);
|
||||
ogs_warn("Not supported version [%s]", message.h.api.version);
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(
|
||||
stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST,
|
||||
|
|
@ -137,7 +137,7 @@ void eir_state_operational(ogs_fsm_t *s, eir_event_t *e)
|
|||
break;
|
||||
|
||||
DEFAULT
|
||||
ogs_error("Invalid HTTP method [%s]", message.h.method);
|
||||
ogs_warn("Invalid HTTP method [%s]", message.h.method);
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(stream,
|
||||
OGS_SBI_HTTP_STATUS_FORBIDDEN, &message,
|
||||
|
|
@ -146,7 +146,7 @@ void eir_state_operational(ogs_fsm_t *s, eir_event_t *e)
|
|||
break;
|
||||
|
||||
DEFAULT
|
||||
ogs_error("Invalid resource name [%s]",
|
||||
ogs_warn("Invalid resource name [%s]",
|
||||
message.h.resource.component[0]);
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(stream,
|
||||
|
|
|
|||
|
|
@ -29,29 +29,55 @@ int eir_initialize(void)
|
|||
|
||||
#define APP_NAME "eir"
|
||||
rv = ogs_app_parse_local_conf(APP_NAME);
|
||||
if (rv != OGS_OK) return rv;
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("Failed to parse EIR application configuration [%d]", rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
ogs_sbi_context_init(OpenAPI_nf_type_5G_EIR);
|
||||
eir_context_init();
|
||||
|
||||
rv = ogs_log_config_domain(
|
||||
ogs_app()->logger.domain, ogs_app()->logger.level);
|
||||
if (rv != OGS_OK) return rv;
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("Failed to configure EIR logging [%d]", rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = ogs_sbi_context_parse_config(APP_NAME, "nrf", "scp");
|
||||
if (rv != OGS_OK) return rv;
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("Failed to parse EIR SBI configuration [%d]", rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = eir_context_parse_config();
|
||||
if (rv != OGS_OK) return rv;
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("Failed to parse EIR configuration [%d]", rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = ogs_dbi_init(ogs_app()->db_uri);
|
||||
if (rv != OGS_OK) return rv;
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("Failed to initialize EIR database connection [%d]", rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = ogs_dbi_eir_init();
|
||||
if (rv != OGS_OK)
|
||||
ogs_warn("EIR database constraints could not be prepared; "
|
||||
"continuing with runtime validation");
|
||||
|
||||
rv = eir_sbi_open();
|
||||
if (rv != OGS_OK) return rv;
|
||||
if (rv != OGS_OK) {
|
||||
ogs_error("Failed to open EIR SBI [%d]", rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
thread = ogs_thread_create(eir_main, NULL);
|
||||
if (!thread) return OGS_ERROR;
|
||||
if (!thread) {
|
||||
ogs_error("Failed to create EIR event thread");
|
||||
return OGS_ERROR;
|
||||
}
|
||||
|
||||
initialized = 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,37 @@
|
|||
#include "sbi-path.h"
|
||||
#include "n5geir-handler.h"
|
||||
|
||||
/* This EIR supports IMEI/IMEISV and IMSI-based SUPIs only. */
|
||||
#define EIR_IMSI_MIN_DIGITS 6
|
||||
|
||||
static bool pei_is_supported(const char *pei)
|
||||
{
|
||||
if (!pei) {
|
||||
ogs_warn("Missing equipment identity");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Select the type before validation to avoid warnings for valid PEIs. */
|
||||
if (!strncmp(pei, OGS_ID_PEI_TYPE_IMEI "-",
|
||||
sizeof(OGS_ID_PEI_TYPE_IMEI "-") - 1))
|
||||
return ogs_id_bcd_is_valid(pei, OGS_ID_PEI_TYPE_IMEI,
|
||||
OGS_MAX_IMEI_BCD_LEN, OGS_MAX_IMEI_BCD_LEN);
|
||||
|
||||
if (!strncmp(pei, OGS_ID_SUPI_TYPE_IMEISV "-",
|
||||
sizeof(OGS_ID_SUPI_TYPE_IMEISV "-") - 1))
|
||||
return ogs_id_bcd_is_valid(pei, OGS_ID_SUPI_TYPE_IMEISV,
|
||||
OGS_MAX_IMEISV_BCD_LEN, OGS_MAX_IMEISV_BCD_LEN);
|
||||
|
||||
ogs_warn("Unsupported PEI type; expected imei- or imeisv-");
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool supi_is_supported(const char *supi)
|
||||
{
|
||||
return ogs_id_bcd_is_valid(supi, OGS_ID_SUPI_TYPE_IMSI,
|
||||
EIR_IMSI_MIN_DIGITS, OGS_MAX_IMSI_BCD_LEN);
|
||||
}
|
||||
|
||||
static OpenAPI_equipment_status_e equipment_status_from_dbi(
|
||||
ogs_dbi_eir_status_t status)
|
||||
{
|
||||
|
|
@ -31,6 +62,7 @@ static OpenAPI_equipment_status_e equipment_status_from_dbi(
|
|||
case OGS_DBI_EIR_STATUS_GREYLISTED:
|
||||
return OpenAPI_equipment_status_GREYLISTED;
|
||||
default:
|
||||
ogs_warn("Unsupported EIR database status [%d]", status);
|
||||
return OpenAPI_equipment_status_NULL;
|
||||
}
|
||||
}
|
||||
|
|
@ -48,7 +80,7 @@ bool eir_n5geir_eic_handle_equipment_status(
|
|||
ogs_assert(recvmsg);
|
||||
|
||||
if (!recvmsg->param.pei) {
|
||||
ogs_error("No PEI in equipment-status request");
|
||||
ogs_warn("No PEI in equipment-status request");
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(stream,
|
||||
OGS_SBI_HTTP_STATUS_BAD_REQUEST, recvmsg,
|
||||
|
|
@ -56,8 +88,8 @@ bool eir_n5geir_eic_handle_equipment_status(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!ogs_dbi_eir_pei_is_valid(recvmsg->param.pei)) {
|
||||
ogs_error("Invalid PEI in equipment-status request");
|
||||
if (!pei_is_supported(recvmsg->param.pei)) {
|
||||
ogs_warn("Invalid PEI in equipment-status request");
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(stream,
|
||||
OGS_SBI_HTTP_STATUS_BAD_REQUEST, recvmsg,
|
||||
|
|
@ -66,8 +98,8 @@ bool eir_n5geir_eic_handle_equipment_status(
|
|||
}
|
||||
|
||||
if (recvmsg->param.supi &&
|
||||
!ogs_dbi_eir_supi_is_valid(recvmsg->param.supi)) {
|
||||
ogs_error("Invalid SUPI in equipment-status request");
|
||||
!supi_is_supported(recvmsg->param.supi)) {
|
||||
ogs_warn("Invalid SUPI in equipment-status request");
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(stream,
|
||||
OGS_SBI_HTTP_STATUS_BAD_REQUEST, recvmsg,
|
||||
|
|
@ -92,7 +124,8 @@ bool eir_n5geir_eic_handle_equipment_status(
|
|||
return true;
|
||||
|
||||
case OGS_NOTFOUND:
|
||||
ogs_info("Unknown equipment [supi_presence:%s]",
|
||||
ogs_info("Unknown equipment [pei:%s,supi_presence:%s]",
|
||||
recvmsg->param.pei,
|
||||
recvmsg->param.supi ? "true" : "false");
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(stream,
|
||||
|
|
@ -101,7 +134,8 @@ bool eir_n5geir_eic_handle_equipment_status(
|
|||
return false;
|
||||
|
||||
default:
|
||||
ogs_error("EIR database error");
|
||||
ogs_warn("EIR database lookup failed [pei:%s,rv:%d]",
|
||||
recvmsg->param.pei, rv);
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(stream,
|
||||
OGS_SBI_HTTP_STATUS_INTERNAL_SERVER_ERROR, recvmsg,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ static bool nudr_validate_imsi_supi(
|
|||
}
|
||||
|
||||
if (ogs_imsi_bcd_is_valid(value) == false) {
|
||||
ogs_error("Invalid IMSI SUPI [%s]", supi);
|
||||
ogs_assert(true ==
|
||||
ogs_sbi_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST,
|
||||
recvmsg, "Invalid SUPI", supi, NULL));
|
||||
|
|
|
|||
|
|
@ -124,7 +124,9 @@ int app_initialize(const char *const argv[])
|
|||
if (ogs_global_conf()->parameter.no_udr == 0)
|
||||
run_threads("udr", ogs_global_conf()->parameter.udr_count,
|
||||
argv_out, i, udr_threads);
|
||||
if (ogs_global_conf()->parameter.no_eir == 0)
|
||||
|
||||
if (ogs_global_conf()->parameter.no_eir == 0 &&
|
||||
ogs_global_conf()->parameter.eir_count > 0)
|
||||
run_threads("eir", ogs_global_conf()->parameter.eir_count,
|
||||
argv_out, i, eir_threads);
|
||||
|
||||
|
|
@ -155,6 +157,10 @@ void app_terminate(void)
|
|||
ogs_thread_destroy(upf_threads[i]);
|
||||
upf_threads[i] = NULL;
|
||||
}
|
||||
if (eir_threads[i]) {
|
||||
ogs_thread_destroy(eir_threads[i]);
|
||||
eir_threads[i] = NULL;
|
||||
}
|
||||
if (udr_threads[i]) {
|
||||
ogs_thread_destroy(udr_threads[i]);
|
||||
udr_threads[i] = NULL;
|
||||
|
|
|
|||
|
|
@ -1580,83 +1580,6 @@ int test_db_remove_ue(test_ue_t *test_ue)
|
|||
return OGS_OK;
|
||||
}
|
||||
|
||||
int test_db_insert_eir(const char *pei, const char *supi, const char *status)
|
||||
{
|
||||
mongoc_collection_t *collection = NULL;
|
||||
bson_t *doc = NULL;
|
||||
bson_error_t error;
|
||||
|
||||
ogs_assert(pei);
|
||||
ogs_assert(status);
|
||||
|
||||
collection = mongoc_client_get_collection(
|
||||
ogs_mongoc()->client, ogs_mongoc()->name, "eir");
|
||||
if (!collection) {
|
||||
ogs_error("mongoc_client_get_collection() failed");
|
||||
return OGS_ERROR;
|
||||
}
|
||||
|
||||
if (supi)
|
||||
doc = BCON_NEW(
|
||||
"pei", BCON_UTF8(pei),
|
||||
"supi", BCON_UTF8(supi),
|
||||
"status", BCON_UTF8(status));
|
||||
else
|
||||
doc = BCON_NEW(
|
||||
"pei", BCON_UTF8(pei),
|
||||
"status", BCON_UTF8(status));
|
||||
ogs_assert(doc);
|
||||
|
||||
if (mongoc_collection_insert(collection,
|
||||
MONGOC_INSERT_NONE, doc, NULL, &error) != true) {
|
||||
ogs_error("mongoc_collection_insert() failed");
|
||||
bson_destroy(doc);
|
||||
mongoc_collection_destroy(collection);
|
||||
return OGS_ERROR;
|
||||
}
|
||||
bson_destroy(doc);
|
||||
|
||||
mongoc_collection_destroy(collection);
|
||||
|
||||
return OGS_OK;
|
||||
}
|
||||
|
||||
int test_db_remove_eir(const char *pei, const char *supi)
|
||||
{
|
||||
mongoc_collection_t *collection = NULL;
|
||||
bson_t *key = NULL;
|
||||
bson_error_t error;
|
||||
|
||||
ogs_assert(pei);
|
||||
|
||||
collection = mongoc_client_get_collection(
|
||||
ogs_mongoc()->client, ogs_mongoc()->name, "eir");
|
||||
if (!collection) {
|
||||
ogs_error("mongoc_client_get_collection() failed");
|
||||
return OGS_ERROR;
|
||||
}
|
||||
|
||||
if (supi)
|
||||
key = BCON_NEW("pei", BCON_UTF8(pei), "supi", BCON_UTF8(supi));
|
||||
else
|
||||
key = BCON_NEW("pei", BCON_UTF8(pei),
|
||||
"supi", "{", "$exists", BCON_BOOL(false), "}");
|
||||
ogs_assert(key);
|
||||
|
||||
if (mongoc_collection_remove(collection,
|
||||
MONGOC_REMOVE_NONE, key, NULL, &error) != true) {
|
||||
ogs_error("mongoc_collection_remove() failed");
|
||||
bson_destroy(key);
|
||||
mongoc_collection_destroy(collection);
|
||||
return OGS_ERROR;
|
||||
}
|
||||
bson_destroy(key);
|
||||
|
||||
mongoc_collection_destroy(collection);
|
||||
|
||||
return OGS_OK;
|
||||
}
|
||||
|
||||
bson_t *test_db_new_simple(test_ue_t *test_ue)
|
||||
{
|
||||
bson_t *doc = NULL;
|
||||
|
|
|
|||
|
|
@ -405,6 +405,7 @@ typedef struct test_ue_s {
|
|||
/* 5GC: Last received message */
|
||||
S1AP_ProcedureCode_t ngap_procedure_code;
|
||||
uint8_t gmm_message_type;
|
||||
ogs_nas_5gmm_cause_t registration_reject_cause;
|
||||
uint8_t gsm_message_type;
|
||||
|
||||
/* EPC: Last received message */
|
||||
|
|
@ -532,9 +533,6 @@ test_bearer_t *test_qos_flow_find_by_qfi(test_sess_t *sess, uint8_t qfi);
|
|||
int test_db_insert_ue(test_ue_t *test_ue, bson_t *doc);
|
||||
int test_db_remove_ue(test_ue_t *test_ue);
|
||||
|
||||
int test_db_insert_eir(const char *pei, const char *supi, const char *status);
|
||||
int test_db_remove_eir(const char *pei, const char *supi);
|
||||
|
||||
bson_t *test_db_new_simple(test_ue_t *test_ue);
|
||||
bson_t *test_db_new_qos_flow(test_ue_t *test_ue);
|
||||
bson_t *test_db_new_qos_flow_bi_directional(test_ue_t *test_ue);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ void testgmm_recv(test_ue_t *test_ue, ogs_pkbuf_t *pkbuf)
|
|||
&message.gmm.registration_request);
|
||||
break;
|
||||
case OGS_NAS_5GS_REGISTRATION_REJECT:
|
||||
test_ue->registration_reject_cause =
|
||||
message.gmm.registration_reject.gmm_cause;
|
||||
break;
|
||||
case OGS_NAS_5GS_SERVICE_ACCEPT:
|
||||
testgmm_handle_service_accept(test_ue, &message.gmm.service_accept);
|
||||
|
|
|
|||
388
tests/eir/dbi-test.c
Normal file
388
tests/eir/dbi-test.c
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
/*
|
||||
* Copyright (C) 2026 by Sukchan Lee <acetcom@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "integration.h"
|
||||
|
||||
#define TEST_EIR_PEI "imeisv-4901542032375186"
|
||||
#define TEST_EIR_IMEI "imei-490154203237518"
|
||||
#define TEST_EIR_SUPI "imsi-001010123456789"
|
||||
#define TEST_EIR_OTHER_SUPI "imsi-001010987654321"
|
||||
|
||||
#define GENERIC(status) \
|
||||
"{\"pei\":\"" TEST_EIR_PEI "\",\"status\":\"" status "\"}"
|
||||
#define SPECIFIC(status) \
|
||||
"{\"pei\":\"" TEST_EIR_PEI "\",\"supi\":\"" TEST_EIR_SUPI \
|
||||
"\",\"status\":\"" status "\"}"
|
||||
#define NULL_SUPI(status) \
|
||||
"{\"pei\":\"" TEST_EIR_PEI "\",\"supi\":null,\"status\":\"" status "\"}"
|
||||
|
||||
|
||||
typedef struct lookup_case_s {
|
||||
const char *name;
|
||||
const char *documents[2];
|
||||
const char *pei;
|
||||
const char *supi;
|
||||
int result;
|
||||
ogs_dbi_eir_status_t status;
|
||||
const char *matched_supi;
|
||||
} lookup_case_t;
|
||||
|
||||
static const lookup_case_t lookup_cases[] = {
|
||||
{
|
||||
"generic whitelist", {GENERIC("WHITELISTED")},
|
||||
TEST_EIR_PEI, NULL, OGS_OK, OGS_DBI_EIR_STATUS_WHITELISTED,
|
||||
NULL
|
||||
}, {
|
||||
"generic blacklist", {GENERIC("BLACKLISTED")},
|
||||
TEST_EIR_PEI, NULL, OGS_OK, OGS_DBI_EIR_STATUS_BLACKLISTED,
|
||||
NULL
|
||||
}, {
|
||||
"generic greylist", {GENERIC("GREYLISTED")},
|
||||
TEST_EIR_PEI, TEST_EIR_SUPI, OGS_OK, OGS_DBI_EIR_STATUS_GREYLISTED,
|
||||
NULL
|
||||
}, {
|
||||
"IMEI lookup",
|
||||
{"{\"pei\":\"" TEST_EIR_IMEI "\",\"status\":\"WHITELISTED\"}"},
|
||||
TEST_EIR_IMEI, NULL, OGS_OK, OGS_DBI_EIR_STATUS_WHITELISTED,
|
||||
NULL
|
||||
}, {
|
||||
"pair blacklist overrides generic whitelist",
|
||||
{GENERIC("WHITELISTED"), SPECIFIC("BLACKLISTED")},
|
||||
TEST_EIR_PEI, TEST_EIR_SUPI, OGS_OK, OGS_DBI_EIR_STATUS_BLACKLISTED,
|
||||
TEST_EIR_SUPI
|
||||
}, {
|
||||
"pair whitelist overrides generic blacklist",
|
||||
{GENERIC("BLACKLISTED"), SPECIFIC("WHITELISTED")},
|
||||
TEST_EIR_PEI, TEST_EIR_SUPI, OGS_OK, OGS_DBI_EIR_STATUS_WHITELISTED,
|
||||
TEST_EIR_SUPI
|
||||
}, {
|
||||
"pair-only exact match", {SPECIFIC("GREYLISTED")},
|
||||
TEST_EIR_PEI, TEST_EIR_SUPI, OGS_OK, OGS_DBI_EIR_STATUS_GREYLISTED,
|
||||
TEST_EIR_SUPI
|
||||
}, {
|
||||
"different SUPI uses generic record",
|
||||
{SPECIFIC("BLACKLISTED"), GENERIC("WHITELISTED")},
|
||||
TEST_EIR_PEI, TEST_EIR_OTHER_SUPI, OGS_OK,
|
||||
OGS_DBI_EIR_STATUS_WHITELISTED, NULL
|
||||
}, {
|
||||
"omitted SUPI ignores pair",
|
||||
{GENERIC("GREYLISTED"), SPECIFIC("BLACKLISTED")},
|
||||
TEST_EIR_PEI, NULL, OGS_OK, OGS_DBI_EIR_STATUS_GREYLISTED,
|
||||
NULL
|
||||
}, {
|
||||
"null SUPI is generic", {NULL_SUPI("BLACKLISTED")},
|
||||
TEST_EIR_PEI, NULL, OGS_OK, OGS_DBI_EIR_STATUS_BLACKLISTED,
|
||||
NULL
|
||||
}, {
|
||||
"null SUPI fallback", {NULL_SUPI("WHITELISTED")},
|
||||
TEST_EIR_PEI, TEST_EIR_SUPI, OGS_OK, OGS_DBI_EIR_STATUS_WHITELISTED,
|
||||
NULL
|
||||
}, {
|
||||
"unknown equipment", {NULL},
|
||||
TEST_EIR_PEI, TEST_EIR_SUPI, OGS_NOTFOUND,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"unrelated PEI", {GENERIC("WHITELISTED")},
|
||||
TEST_EIR_IMEI, TEST_EIR_SUPI, OGS_NOTFOUND,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"pair-only other SUPI", {SPECIFIC("WHITELISTED")},
|
||||
TEST_EIR_PEI, TEST_EIR_OTHER_SUPI, OGS_NOTFOUND,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"pair-only omitted SUPI", {SPECIFIC("WHITELISTED")},
|
||||
TEST_EIR_PEI, NULL, OGS_NOTFOUND,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"invalid status is a database error", {GENERIC("BOGUS")},
|
||||
TEST_EIR_PEI, NULL, OGS_ERROR,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"empty status is a database error", {GENERIC("")},
|
||||
TEST_EIR_PEI, NULL, OGS_ERROR,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"missing status is a database error",
|
||||
{"{\"pei\":\"" TEST_EIR_PEI "\"}"},
|
||||
TEST_EIR_PEI, NULL, OGS_ERROR,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"numeric status is a database error",
|
||||
{"{\"pei\":\"" TEST_EIR_PEI "\",\"status\":1}"},
|
||||
TEST_EIR_PEI, NULL, OGS_ERROR,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"null status is a database error",
|
||||
{"{\"pei\":\"" TEST_EIR_PEI "\",\"status\":null}"},
|
||||
TEST_EIR_PEI, NULL, OGS_ERROR,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"SUPI array match is a database error",
|
||||
{"{\"pei\":\"" TEST_EIR_PEI "\",\"supi\":[\"" TEST_EIR_SUPI
|
||||
"\"],\"status\":\"WHITELISTED\"}"},
|
||||
TEST_EIR_PEI, TEST_EIR_SUPI, OGS_ERROR,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"null SUPI array is not a generic record",
|
||||
{"{\"pei\":\"" TEST_EIR_PEI
|
||||
"\",\"supi\":[null],\"status\":\"WHITELISTED\"}"},
|
||||
TEST_EIR_PEI, NULL, OGS_ERROR,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}, {
|
||||
"PEI array match is a database error",
|
||||
{"{\"pei\":[\"" TEST_EIR_PEI
|
||||
"\"],\"status\":\"WHITELISTED\"}"},
|
||||
TEST_EIR_PEI, NULL, OGS_ERROR,
|
||||
OGS_DBI_EIR_STATUS_UNKNOWN, NULL
|
||||
}
|
||||
};
|
||||
|
||||
typedef struct fixture_s {
|
||||
bson_oid_t ids[3];
|
||||
unsigned int count;
|
||||
} fixture_t;
|
||||
|
||||
static bool fixture_begin(abts_case *tc)
|
||||
{
|
||||
bson_error_t error;
|
||||
bson_t *query = BCON_NEW("pei", "{", "$in", "[",
|
||||
BCON_UTF8(TEST_EIR_PEI), BCON_UTF8(TEST_EIR_IMEI), "]", "}");
|
||||
int64_t count = mongoc_collection_count_documents(
|
||||
ogs_mongoc()->collection.eir, query, NULL, NULL, NULL, &error);
|
||||
|
||||
bson_destroy(query);
|
||||
if (count != 0)
|
||||
ABTS_FAIL(tc, count < 0 ? error.message :
|
||||
"EIR DB fixture PEI already exists; existing records kept");
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
static bool fixture_write(abts_case *tc, fixture_t *fixture,
|
||||
const char *json, const bson_t *opts, int expected_error)
|
||||
{
|
||||
bson_error_t error;
|
||||
bson_t *document = bson_new_from_json((const uint8_t *)json, -1, &error);
|
||||
unsigned int index = fixture->count++;
|
||||
bool inserted;
|
||||
|
||||
ogs_assert(index < OGS_ARRAY_SIZE(fixture->ids));
|
||||
bson_oid_init(&fixture->ids[index], NULL);
|
||||
if (!document) {
|
||||
ABTS_FAIL(tc, error.message);
|
||||
return false;
|
||||
}
|
||||
BSON_APPEND_OID(document, "_id", &fixture->ids[index]);
|
||||
inserted = mongoc_collection_insert_one(ogs_mongoc()->collection.eir,
|
||||
document, opts, NULL, &error);
|
||||
bson_destroy(document);
|
||||
|
||||
if (!inserted && !expected_error) {
|
||||
ABTS_FAIL(tc, error.message);
|
||||
} else {
|
||||
ABTS_INT_EQUAL(tc, expected_error, inserted ? 0 : error.code);
|
||||
}
|
||||
return !tc->failed;
|
||||
}
|
||||
|
||||
static bool fixture_insert(abts_case *tc, fixture_t *fixture,
|
||||
const char *json, const bson_t *opts)
|
||||
{
|
||||
return fixture_write(tc, fixture, json, opts, 0);
|
||||
}
|
||||
|
||||
static void fixture_end(abts_case *tc, fixture_t *fixture)
|
||||
{
|
||||
bson_error_t error;
|
||||
|
||||
/* Remove only documents created by this fixture. */
|
||||
while (fixture->count) {
|
||||
bson_t *query = BCON_NEW("_id",
|
||||
BCON_OID(&fixture->ids[--fixture->count]));
|
||||
|
||||
if (!mongoc_collection_delete_one(ogs_mongoc()->collection.eir,
|
||||
query, NULL, NULL, &error))
|
||||
ABTS_FAIL(tc, error.message);
|
||||
bson_destroy(query);
|
||||
}
|
||||
}
|
||||
|
||||
static void equipment_lookup(abts_case *tc, void *data)
|
||||
{
|
||||
const lookup_case_t *test = data;
|
||||
fixture_t fixture = {0};
|
||||
ogs_dbi_eir_record_t record;
|
||||
bson_t *opts = NULL;
|
||||
unsigned int i;
|
||||
int rv;
|
||||
|
||||
ogs_debug("EIR DB: %s", test->name);
|
||||
if (!fixture_begin(tc))
|
||||
return;
|
||||
/* Exercise runtime validation even if an operator installed a validator. */
|
||||
if (test->result == OGS_ERROR)
|
||||
opts = BCON_NEW("bypassDocumentValidation", BCON_BOOL(true));
|
||||
for (i = 0; i < OGS_ARRAY_SIZE(test->documents); i++) {
|
||||
if (test->documents[i] &&
|
||||
!fixture_insert(tc, &fixture, test->documents[i], opts))
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
memset(&record, 0, sizeof(record));
|
||||
record.status = OGS_DBI_EIR_STATUS_BLACKLISTED;
|
||||
rv = ogs_dbi_eir_check_equipment(test->supi, test->pei, &record);
|
||||
ABTS_INT_EQUAL(tc, test->result, rv);
|
||||
ABTS_INT_EQUAL(tc, test->status, record.status);
|
||||
if (rv == OGS_OK) {
|
||||
ABTS_STR_EQUAL(tc, test->pei, record.pei);
|
||||
ABTS_STR_EQUAL(tc, test->matched_supi, record.supi);
|
||||
} else {
|
||||
ABTS_PTR_EQUAL(tc, NULL, record.pei);
|
||||
ABTS_PTR_EQUAL(tc, NULL, record.supi);
|
||||
}
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
ABTS_PTR_EQUAL(tc, NULL, record.pei);
|
||||
ABTS_PTR_EQUAL(tc, NULL, record.supi);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_UNKNOWN, record.status);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
cleanup:
|
||||
if (opts)
|
||||
bson_destroy(opts);
|
||||
fixture_end(tc, &fixture);
|
||||
}
|
||||
|
||||
static void invalid_pair_does_not_fall_back(abts_case *tc, void *data)
|
||||
{
|
||||
fixture_t fixture = {0};
|
||||
ogs_dbi_eir_record_t record;
|
||||
bson_t *opts;
|
||||
bool inserted;
|
||||
int rv;
|
||||
|
||||
if (!fixture_begin(tc))
|
||||
return;
|
||||
if (!fixture_insert(tc, &fixture, GENERIC("WHITELISTED"), NULL))
|
||||
goto cleanup;
|
||||
opts = BCON_NEW("bypassDocumentValidation", BCON_BOOL(true));
|
||||
inserted = fixture_insert(tc, &fixture, SPECIFIC("INVALID"), opts);
|
||||
bson_destroy(opts);
|
||||
if (!inserted)
|
||||
goto cleanup;
|
||||
|
||||
/* Simulate legacy/corrupt data without changing collection validation. */
|
||||
rv = ogs_dbi_eir_check_equipment(TEST_EIR_SUPI, TEST_EIR_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_ERROR, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_UNKNOWN, record.status);
|
||||
ABTS_PTR_EQUAL(tc, NULL, record.pei);
|
||||
ABTS_PTR_EQUAL(tc, NULL, record.supi);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
cleanup:
|
||||
fixture_end(tc, &fixture);
|
||||
}
|
||||
|
||||
enum {
|
||||
EIR_DUPLICATE_KEY = 11000,
|
||||
EIR_DOCUMENT_VALIDATION_FAILURE = 121
|
||||
};
|
||||
|
||||
typedef struct write_case_s {
|
||||
const char *documents[2];
|
||||
int error;
|
||||
} write_case_t;
|
||||
|
||||
static const write_case_t write_cases[] = {
|
||||
{{GENERIC("WHITELISTED"), GENERIC("BLACKLISTED")}, EIR_DUPLICATE_KEY},
|
||||
{{GENERIC("WHITELISTED"), NULL_SUPI("BLACKLISTED")},
|
||||
EIR_DUPLICATE_KEY},
|
||||
{{NULL_SUPI("WHITELISTED"), GENERIC("BLACKLISTED")},
|
||||
EIR_DUPLICATE_KEY},
|
||||
{{SPECIFIC("WHITELISTED"), SPECIFIC("BLACKLISTED")}, EIR_DUPLICATE_KEY},
|
||||
{{GENERIC("WHITELISTED"), SPECIFIC("BLACKLISTED")}, 0},
|
||||
{{SPECIFIC("WHITELISTED"),
|
||||
"{\"pei\":\"" TEST_EIR_PEI "\",\"supi\":\"" TEST_EIR_OTHER_SUPI
|
||||
"\",\"status\":\"BLACKLISTED\"}"}, 0},
|
||||
{{NULL, GENERIC("INVALID")}, EIR_DOCUMENT_VALIDATION_FAILURE},
|
||||
{{NULL, "{\"pei\":\"" TEST_EIR_PEI "\"}"}, EIR_DOCUMENT_VALIDATION_FAILURE},
|
||||
{{NULL, "{\"pei\":\"" TEST_EIR_PEI "\",\"status\":1}"},
|
||||
EIR_DOCUMENT_VALIDATION_FAILURE},
|
||||
{{NULL, "{\"pei\":\"" TEST_EIR_PEI "\",\"status\":null}"},
|
||||
EIR_DOCUMENT_VALIDATION_FAILURE},
|
||||
{{NULL, "{\"status\":\"WHITELISTED\"}"}, EIR_DOCUMENT_VALIDATION_FAILURE},
|
||||
{{NULL, "{\"pei\":1,\"status\":\"WHITELISTED\"}"},
|
||||
EIR_DOCUMENT_VALIDATION_FAILURE},
|
||||
{{NULL, "{\"pei\":[\"" TEST_EIR_PEI
|
||||
"\"],\"status\":\"WHITELISTED\"}"}, EIR_DOCUMENT_VALIDATION_FAILURE},
|
||||
{{NULL, "{\"pei\":\"" TEST_EIR_PEI "\",\"supi\":1,"
|
||||
"\"status\":\"WHITELISTED\"}"}, EIR_DOCUMENT_VALIDATION_FAILURE},
|
||||
{{NULL, "{\"pei\":\"" TEST_EIR_PEI "\",\"supi\":[\"" TEST_EIR_SUPI
|
||||
"\"],\"status\":\"WHITELISTED\"}"}, EIR_DOCUMENT_VALIDATION_FAILURE}
|
||||
};
|
||||
|
||||
static void database_constraints(abts_case *tc, void *data)
|
||||
{
|
||||
const write_case_t *test = data;
|
||||
fixture_t fixture = {0};
|
||||
|
||||
if (!fixture_begin(tc))
|
||||
return;
|
||||
if (test->documents[0] && !fixture_insert(tc, &fixture,
|
||||
test->documents[0], NULL))
|
||||
goto cleanup;
|
||||
|
||||
/* Normal writes must enforce the startup index and validator. */
|
||||
fixture_write(tc, &fixture, test->documents[1], NULL, test->error);
|
||||
|
||||
cleanup:
|
||||
fixture_end(tc, &fixture);
|
||||
}
|
||||
|
||||
static void repeated_database_init(abts_case *tc, void *data)
|
||||
{
|
||||
fixture_t fixture = {0};
|
||||
|
||||
if (!fixture_begin(tc))
|
||||
return;
|
||||
if (!fixture_insert(tc, &fixture, GENERIC("WHITELISTED"), NULL))
|
||||
goto cleanup;
|
||||
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, ogs_dbi_eir_init());
|
||||
/* Reinitialization must retain data and both write constraints. */
|
||||
fixture_write(tc, &fixture, NULL_SUPI("BLACKLISTED"), NULL,
|
||||
EIR_DUPLICATE_KEY);
|
||||
fixture_write(tc, &fixture, SPECIFIC("INVALID"), NULL,
|
||||
EIR_DOCUMENT_VALIDATION_FAILURE);
|
||||
|
||||
cleanup:
|
||||
fixture_end(tc, &fixture);
|
||||
}
|
||||
|
||||
abts_suite *test_eir_dbi(abts_suite *suite)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
suite = ADD_SUITE(suite)
|
||||
for (i = 0; i < OGS_ARRAY_SIZE(write_cases); i++)
|
||||
abts_run_test(suite, database_constraints, (void *)&write_cases[i]);
|
||||
abts_run_test(suite, repeated_database_init, NULL);
|
||||
for (i = 0; i < OGS_ARRAY_SIZE(lookup_cases); i++)
|
||||
abts_run_test(suite, equipment_lookup, (void *)&lookup_cases[i]);
|
||||
abts_run_test(suite, invalid_pair_does_not_fall_back, NULL);
|
||||
return suite;
|
||||
}
|
||||
160
tests/eir/integration-main.c
Normal file
160
tests/eir/integration-main.c
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/* Copyright (C) 2026 by Sukchan Lee <acetcom@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
#include "integration.h"
|
||||
#include <curl/curl.h>
|
||||
|
||||
static char *eir_uri;
|
||||
static char *nrf_uri;
|
||||
|
||||
const char *test_eir_sbi_uri(void)
|
||||
{
|
||||
return eir_uri;
|
||||
}
|
||||
|
||||
const char *test_eir_nrf_uri(void)
|
||||
{
|
||||
return nrf_uri;
|
||||
}
|
||||
|
||||
static bool config_member(ogs_yaml_iter_t *parent,
|
||||
const char *name, ogs_yaml_iter_t *child)
|
||||
{
|
||||
ogs_yaml_iter_t iter = *parent;
|
||||
|
||||
if (ogs_yaml_iter_type(&iter) != YAML_MAPPING_NODE)
|
||||
return false;
|
||||
|
||||
while (ogs_yaml_iter_next(&iter)) {
|
||||
if (!strcmp(ogs_yaml_iter_key(&iter), name)) {
|
||||
ogs_yaml_iter_recurse(&iter, child);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static char *config_sbi_uri(const char *nf)
|
||||
{
|
||||
ogs_yaml_iter_t node, server, address, port;
|
||||
const char *host, *value;
|
||||
const char *field = "section";
|
||||
const char *reason = "missing or invalid mapping";
|
||||
char *end = NULL, *uri;
|
||||
unsigned long number;
|
||||
|
||||
ogs_yaml_iter_init(&node, ogs_app()->document);
|
||||
if (!config_member(&node, nf, &node))
|
||||
goto invalid;
|
||||
field = "sbi";
|
||||
if (!config_member(&node, "sbi", &node))
|
||||
goto invalid;
|
||||
field = "sbi.server";
|
||||
if (!config_member(&node, "server", &node))
|
||||
goto invalid;
|
||||
|
||||
if (ogs_yaml_iter_type(&node) == YAML_SEQUENCE_NODE) {
|
||||
reason = "empty server list";
|
||||
if (!ogs_yaml_iter_next(&node))
|
||||
goto invalid;
|
||||
ogs_yaml_iter_recurse(&node, &server);
|
||||
reason = "multiple servers are not supported by this test";
|
||||
if (ogs_yaml_iter_next(&node))
|
||||
goto invalid;
|
||||
node = server;
|
||||
}
|
||||
|
||||
field = "sbi.server.address";
|
||||
reason = "missing or non-scalar value";
|
||||
if (!config_member(&node, "address", &address) ||
|
||||
ogs_yaml_iter_type(&address) != YAML_SCALAR_NODE)
|
||||
goto invalid;
|
||||
host = ogs_yaml_iter_value(&address);
|
||||
reason = "empty address";
|
||||
if (!*host)
|
||||
goto invalid;
|
||||
|
||||
field = "sbi.server.port";
|
||||
reason = "missing or non-scalar value";
|
||||
if (!config_member(&node, "port", &port) ||
|
||||
ogs_yaml_iter_type(&port) != YAML_SCALAR_NODE)
|
||||
goto invalid;
|
||||
value = ogs_yaml_iter_value(&port);
|
||||
number = strtoul(value, &end, 10);
|
||||
reason = "port must be an integer between 1 and 65535";
|
||||
if (!*value || !end || *end || !number || number > 65535)
|
||||
goto invalid;
|
||||
|
||||
uri = strchr(host, ':') ?
|
||||
ogs_msprintf("http://[%s]:%lu", host, number) :
|
||||
ogs_msprintf("http://%s:%lu", host, number);
|
||||
if (!uri)
|
||||
ogs_error("Cannot allocate EIR integration SBI URI for %s", nf);
|
||||
return uri;
|
||||
|
||||
invalid:
|
||||
ogs_error("Invalid EIR integration config [%s.%s]: %s",
|
||||
nf, field, reason);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void terminate(void)
|
||||
{
|
||||
ogs_msleep(50);
|
||||
|
||||
test_child_terminate();
|
||||
app_terminate();
|
||||
test_5gc_final();
|
||||
|
||||
ogs_free(eir_uri);
|
||||
ogs_free(nrf_uri);
|
||||
ogs_app_terminate();
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
static void initialize(const char *const argv[])
|
||||
{
|
||||
int rv;
|
||||
|
||||
rv = ogs_app_initialize(NULL, NULL, argv);
|
||||
if (rv != OGS_OK)
|
||||
ogs_error("EIR integration application setup failed [error:%d]", rv);
|
||||
ogs_assert(rv == OGS_OK);
|
||||
test_5gc_init();
|
||||
|
||||
eir_uri = config_sbi_uri("eir");
|
||||
nrf_uri = config_sbi_uri("nrf");
|
||||
ogs_assert(eir_uri && nrf_uri);
|
||||
|
||||
rv = app_initialize(argv);
|
||||
if (rv != OGS_OK)
|
||||
ogs_error("EIR integration NF startup failed [error:%d]", rv);
|
||||
ogs_assert(rv == OGS_OK);
|
||||
}
|
||||
|
||||
int main(int argc, const char *const argv[])
|
||||
{
|
||||
abts_suite *suite = NULL;
|
||||
CURLcode rv;
|
||||
|
||||
rv = curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
if (rv != CURLE_OK) {
|
||||
fprintf(stderr, "EIR integration curl initialization failed: %s\n",
|
||||
curl_easy_strerror(rv));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
atexit(terminate);
|
||||
test_app_run(argc, argv, "eir.yaml", initialize);
|
||||
|
||||
/* Discovery confirms that EIR registered its SBI service with the NRF. */
|
||||
if (!test_eir_wait_ready()) {
|
||||
ogs_error("EIR did not become ready before the integration timeout");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
suite = test_eir_dbi(suite);
|
||||
suite = test_eir_service(suite);
|
||||
suite = test_eir_registration(suite);
|
||||
return abts_report(suite);
|
||||
}
|
||||
18
tests/eir/integration.h
Normal file
18
tests/eir/integration.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* Copyright (C) 2026 by Sukchan Lee <acetcom@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
#ifndef TEST_EIR_INTEGRATION_H
|
||||
#define TEST_EIR_INTEGRATION_H
|
||||
|
||||
#include "test-app.h"
|
||||
|
||||
/* Addresses come from the same configuration passed to every child NF. */
|
||||
const char *test_eir_sbi_uri(void);
|
||||
const char *test_eir_nrf_uri(void);
|
||||
bool test_eir_wait_ready(void);
|
||||
|
||||
abts_suite *test_eir_dbi(abts_suite *suite);
|
||||
abts_suite *test_eir_service(abts_suite *suite);
|
||||
abts_suite *test_eir_registration(abts_suite *suite);
|
||||
|
||||
#endif /* TEST_EIR_INTEGRATION_H */
|
||||
17
tests/eir/meson.build
Normal file
17
tests/eir/meson.build
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright (C) 2026 by Sukchan Lee <acetcom@gmail.com>
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
test_eir_integration_exe = executable('eir-integration',
|
||||
sources : files('''
|
||||
integration-main.c
|
||||
dbi-test.c
|
||||
service-test.c
|
||||
registration-test.c
|
||||
'''.split()),
|
||||
c_args : testunit_core_cc_flags,
|
||||
dependencies : [libtest5gc_dep, libcurl_dep])
|
||||
|
||||
test('eir-integration', test_eir_integration_exe,
|
||||
is_parallel : false,
|
||||
timeout : 180,
|
||||
suite : ['eir', '5gc'])
|
||||
321
tests/eir/registration-test.c
Normal file
321
tests/eir/registration-test.c
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* Copyright (C) 2026 by Sukchan Lee <acetcom@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "integration.h"
|
||||
|
||||
typedef struct registration_case_s {
|
||||
const char *name;
|
||||
const char *generic_status;
|
||||
const char *specific_status;
|
||||
bool other_supi;
|
||||
uint8_t reject_cause;
|
||||
} registration_case_t;
|
||||
|
||||
static const registration_case_t registration_cases[] = {
|
||||
{ "whitelist registration", "WHITELISTED", NULL, false, 0 },
|
||||
{ "greylist registration", "GREYLISTED", NULL, false, 0 },
|
||||
{ "blacklist rejects registration", "BLACKLISTED", NULL, false,
|
||||
OGS_5GMM_CAUSE_ILLEGAL_ME },
|
||||
{ "unknown equipment rejects registration", NULL, NULL, false,
|
||||
OGS_5GMM_CAUSE_5GS_SERVICES_NOT_ALLOWED },
|
||||
{ "specific blacklist overrides generic whitelist",
|
||||
"WHITELISTED", "BLACKLISTED", false, OGS_5GMM_CAUSE_ILLEGAL_ME },
|
||||
{ "specific whitelist overrides generic blacklist",
|
||||
"BLACKLISTED", "WHITELISTED", false, 0 },
|
||||
{ "different SUPI falls back to generic whitelist",
|
||||
"WHITELISTED", "BLACKLISTED", true, 0 },
|
||||
{ "EIR server failure rejects registration", "INVALID", NULL, false,
|
||||
OGS_5GMM_CAUSE_PAYLOAD_WAS_NOT_FORWARDED },
|
||||
};
|
||||
|
||||
typedef struct registration_fixture_s {
|
||||
mongoc_collection_t *collection[3];
|
||||
bson_oid_t id[3];
|
||||
unsigned int count;
|
||||
} registration_fixture_t;
|
||||
|
||||
/* Refuse existing identities; the test must never replace another fixture. */
|
||||
static bool identity_unused(abts_case *tc, mongoc_collection_t *collection,
|
||||
const char *key, const char *value)
|
||||
{
|
||||
bson_t *query = BCON_NEW(key, BCON_UTF8(value));
|
||||
mongoc_cursor_t *cursor;
|
||||
const bson_t *document;
|
||||
bson_error_t error;
|
||||
bool found, failed;
|
||||
|
||||
ogs_assert(query);
|
||||
cursor = mongoc_collection_find_with_opts(collection, query, NULL, NULL);
|
||||
ogs_assert(cursor);
|
||||
found = mongoc_cursor_next(cursor, &document);
|
||||
failed = mongoc_cursor_error(cursor, &error);
|
||||
if (failed) {
|
||||
ABTS_FAIL(tc, error.message);
|
||||
} else if (found) {
|
||||
ABTS_FAIL(tc, "EIR registration fixture identity already exists");
|
||||
}
|
||||
mongoc_cursor_destroy(cursor);
|
||||
bson_destroy(query);
|
||||
return !failed && !found;
|
||||
}
|
||||
|
||||
/* Each document is removed by its owned _id, including after a failed step. */
|
||||
static bool insert_fixture(abts_case *tc, registration_fixture_t *fixture,
|
||||
mongoc_collection_t *collection, bson_t *document, const bson_t *opts)
|
||||
{
|
||||
unsigned int index = fixture->count++;
|
||||
bson_error_t error;
|
||||
bool inserted;
|
||||
|
||||
ogs_assert(document);
|
||||
ogs_assert(index < OGS_ARRAY_SIZE(fixture->id));
|
||||
fixture->collection[index] = collection;
|
||||
bson_oid_init(&fixture->id[index], NULL);
|
||||
ogs_assert(BSON_APPEND_OID(document, "_id", &fixture->id[index]));
|
||||
inserted = mongoc_collection_insert_one(collection,
|
||||
document, opts, NULL, &error);
|
||||
bson_destroy(document);
|
||||
if (!inserted)
|
||||
ABTS_FAIL(tc, error.message);
|
||||
return inserted;
|
||||
}
|
||||
|
||||
static void remove_fixtures(abts_case *tc, registration_fixture_t *fixture)
|
||||
{
|
||||
bson_error_t error;
|
||||
|
||||
while (fixture->count) {
|
||||
unsigned int index = --fixture->count;
|
||||
bson_t *query = BCON_NEW("_id", BCON_OID(&fixture->id[index]));
|
||||
|
||||
ogs_assert(query);
|
||||
if (!mongoc_collection_delete_one(fixture->collection[index],
|
||||
query, NULL, NULL, &error))
|
||||
ABTS_FAIL(tc, error.message);
|
||||
bson_destroy(query);
|
||||
}
|
||||
}
|
||||
|
||||
static bool seed_registration(abts_case *tc, test_ue_t *ue, const char *pei,
|
||||
const registration_case_t *test, registration_fixture_t *fixture)
|
||||
{
|
||||
bson_t *document;
|
||||
mongoc_collection_t *subscriber = ogs_mongoc()->collection.subscriber;
|
||||
mongoc_collection_t *eir = ogs_mongoc()->collection.eir;
|
||||
|
||||
if (!identity_unused(tc, subscriber, "imsi", ue->imsi) ||
|
||||
!identity_unused(tc, eir, "pei", pei))
|
||||
return false;
|
||||
|
||||
ogs_hex_from_string(ue->k_string, ue->k, sizeof(ue->k));
|
||||
ogs_hex_from_string(ue->opc_string, ue->opc, sizeof(ue->opc));
|
||||
document = test_db_new_simple(ue);
|
||||
ABTS_PTR_NOTNULL(tc, document);
|
||||
if (!document || !insert_fixture(tc, fixture, subscriber, document, NULL))
|
||||
return false;
|
||||
|
||||
if (test->generic_status) {
|
||||
bson_t *opts = NULL;
|
||||
bool inserted;
|
||||
|
||||
document = BCON_NEW("pei", BCON_UTF8(pei),
|
||||
"status", BCON_UTF8(test->generic_status));
|
||||
|
||||
/* Exercise AMF failure_action against a real EIR HTTP 500. */
|
||||
if (!strcmp(test->generic_status, "INVALID"))
|
||||
opts = BCON_NEW("bypassDocumentValidation", BCON_BOOL(true));
|
||||
inserted = insert_fixture(tc, fixture, eir, document, opts);
|
||||
if (opts)
|
||||
bson_destroy(opts);
|
||||
if (!inserted)
|
||||
return false;
|
||||
}
|
||||
if (test->specific_status) {
|
||||
document = BCON_NEW("pei", BCON_UTF8(pei),
|
||||
"supi", BCON_UTF8(test->other_supi ?
|
||||
"imsi-999709999999999" : ue->supi),
|
||||
"status", BCON_UTF8(test->specific_status));
|
||||
if (!insert_fixture(tc, fixture, eir, document, NULL))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool send_ngap(abts_case *tc, ogs_socknode_t *ngap, ogs_pkbuf_t *buffer)
|
||||
{
|
||||
int rv;
|
||||
|
||||
ABTS_PTR_NOTNULL(tc, buffer);
|
||||
if (!buffer)
|
||||
return false;
|
||||
rv = testgnb_ngap_send(ngap, buffer);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
if (rv != OGS_OK)
|
||||
ogs_pkbuf_free(buffer);
|
||||
return rv == OGS_OK;
|
||||
}
|
||||
|
||||
static bool send_nas(abts_case *tc, ogs_socknode_t *ngap,
|
||||
test_ue_t *ue, ogs_pkbuf_t *nas)
|
||||
{
|
||||
ABTS_PTR_NOTNULL(tc, nas);
|
||||
if (!nas)
|
||||
return false;
|
||||
return send_ngap(tc, ngap, testngap_build_uplink_nas_transport(ue, nas));
|
||||
}
|
||||
|
||||
static bool receive_ngap(abts_case *tc, ogs_socknode_t *ngap,
|
||||
test_ue_t *ue, int procedure, uint8_t nas_type)
|
||||
{
|
||||
ogs_pkbuf_t *buffer = testgnb_ngap_read(ngap);
|
||||
|
||||
ABTS_PTR_NOTNULL(tc, buffer);
|
||||
if (!buffer)
|
||||
return false;
|
||||
ue->gmm_message_type = 0;
|
||||
testngap_recv(ue, buffer);
|
||||
ABTS_INT_EQUAL(tc, procedure, ue->ngap_procedure_code);
|
||||
if (nas_type)
|
||||
ABTS_INT_EQUAL(tc, nas_type, ue->gmm_message_type);
|
||||
return !tc->failed;
|
||||
}
|
||||
|
||||
static void registration_case(abts_case *tc, void *data)
|
||||
{
|
||||
const registration_case_t *test = data;
|
||||
unsigned int index = test - registration_cases;
|
||||
registration_fixture_t fixture = {0};
|
||||
ogs_nas_5gs_mobile_identity_suci_t suci;
|
||||
test_ue_t *ue = NULL;
|
||||
ogs_socknode_t *ngap = NULL;
|
||||
ogs_pkbuf_t *nas = NULL, *container = NULL;
|
||||
char msin[11], pei[24];
|
||||
|
||||
ogs_debug("EIR registration: %s", test->name);
|
||||
|
||||
memset(&suci, 0, sizeof(suci));
|
||||
suci.h.supi_format = OGS_NAS_5GS_SUPI_FORMAT_IMSI;
|
||||
suci.h.type = OGS_NAS_5GS_MOBILE_IDENTITY_SUCI;
|
||||
suci.routing_indicator2 = suci.routing_indicator3 =
|
||||
suci.routing_indicator4 = 0xf;
|
||||
suci.protection_scheme_id = OGS_PROTECTION_SCHEME_NULL;
|
||||
ogs_snprintf(msin, sizeof(msin), "00004708%02u", index);
|
||||
ue = test_ue_add_by_suci(&suci, msin);
|
||||
ogs_assert(ue);
|
||||
ue->nr_cgi.cell_id = 0x40001;
|
||||
ue->nas.registration.ksi = OGS_NAS_KSI_NO_KEY_IS_AVAILABLE;
|
||||
ue->nas.registration.follow_on_request = 1;
|
||||
ue->nas.registration.value = OGS_NAS_5GS_REGISTRATION_TYPE_INITIAL;
|
||||
ue->k_string = "465b5ce8b199b49faa5f0a2ee238a6bc";
|
||||
ue->opc_string = "e8ed289deba952e4283b54e88e6183ca";
|
||||
ue->mobile_identity_imeisv.digit16 = index;
|
||||
ogs_snprintf(pei, sizeof(pei), "imeisv-866507040040530%u", index);
|
||||
if (!seed_registration(tc, ue, pei, test, &fixture))
|
||||
goto cleanup;
|
||||
|
||||
ngap = testngap_client(1, AF_INET);
|
||||
ABTS_PTR_NOTNULL(tc, ngap);
|
||||
if (!ngap)
|
||||
goto cleanup;
|
||||
if (!send_ngap(tc, ngap,
|
||||
testngap_build_ng_setup_request(0x4708 + index, 22)) ||
|
||||
!receive_ngap(tc, ngap, ue, NGAP_ProcedureCode_id_NGSetup, 0))
|
||||
goto cleanup;
|
||||
|
||||
/* The first unprotected request contains only cleartext IEs. */
|
||||
nas = testgmm_build_registration_request(ue, NULL, false, false);
|
||||
ogs_assert(nas);
|
||||
|
||||
/* Carry the additional IEs inside Security Mode Complete's protected
|
||||
* NAS message container, as in tests/registration/identity-test.c. */
|
||||
ue->registration_request_param.gmm_capability = 1;
|
||||
ue->registration_request_param.s1_ue_network_capability = 1;
|
||||
ue->registration_request_param.requested_nssai = 1;
|
||||
ue->registration_request_param.last_visited_registered_tai = 1;
|
||||
ue->registration_request_param.ue_usage_setting = 1;
|
||||
container = testgmm_build_registration_request(ue, NULL, false, false);
|
||||
ogs_assert(container);
|
||||
if (!send_ngap(tc, ngap, testngap_build_initial_ue_message(ue, nas,
|
||||
NGAP_RRCEstablishmentCause_mo_Signalling, false, true)))
|
||||
goto cleanup;
|
||||
if (!receive_ngap(tc, ngap, ue, NGAP_ProcedureCode_id_DownlinkNASTransport,
|
||||
OGS_NAS_5GS_AUTHENTICATION_REQUEST) ||
|
||||
!send_nas(tc, ngap, ue,
|
||||
testgmm_build_authentication_response(ue)) ||
|
||||
!receive_ngap(tc, ngap, ue,
|
||||
NGAP_ProcedureCode_id_DownlinkNASTransport,
|
||||
OGS_NAS_5GS_SECURITY_MODE_COMMAND))
|
||||
goto cleanup;
|
||||
nas = testgmm_build_security_mode_complete(ue, container);
|
||||
container = NULL;
|
||||
if (!send_nas(tc, ngap, ue, nas))
|
||||
goto cleanup;
|
||||
|
||||
if (test->reject_cause) {
|
||||
if (!receive_ngap(tc, ngap, ue,
|
||||
NGAP_ProcedureCode_id_DownlinkNASTransport,
|
||||
OGS_NAS_5GS_REGISTRATION_REJECT))
|
||||
goto cleanup;
|
||||
ABTS_INT_EQUAL(tc, test->reject_cause, ue->registration_reject_cause);
|
||||
} else {
|
||||
if (!receive_ngap(tc, ngap, ue,
|
||||
NGAP_ProcedureCode_id_InitialContextSetup,
|
||||
OGS_NAS_5GS_REGISTRATION_ACCEPT) ||
|
||||
!send_ngap(tc, ngap,
|
||||
testngap_build_ue_radio_capability_info_indication(ue)) ||
|
||||
!send_ngap(tc, ngap,
|
||||
testngap_build_initial_context_setup_response(ue, false)) ||
|
||||
!send_nas(tc, ngap, ue,
|
||||
testgmm_build_registration_complete(ue)) ||
|
||||
!receive_ngap(tc, ngap, ue,
|
||||
NGAP_ProcedureCode_id_DownlinkNASTransport,
|
||||
OGS_NAS_5GS_CONFIGURATION_UPDATE_COMMAND))
|
||||
goto cleanup;
|
||||
/* Switch-off deregistration releases AMF/UDM/PCF state without a PDU
|
||||
* session, so these tests do not require an SMF, UPF or TUN device. */
|
||||
if (!send_nas(tc, ngap, ue,
|
||||
testgmm_build_de_registration_request(
|
||||
ue, true, true, false)))
|
||||
goto cleanup;
|
||||
}
|
||||
if (!receive_ngap(tc, ngap, ue,
|
||||
NGAP_ProcedureCode_id_UEContextRelease, 0) ||
|
||||
!send_ngap(tc, ngap,
|
||||
testngap_build_ue_context_release_complete(ue)))
|
||||
goto cleanup;
|
||||
|
||||
cleanup:
|
||||
if (container)
|
||||
ogs_pkbuf_free(container);
|
||||
if (ngap)
|
||||
testgnb_ngap_close(ngap);
|
||||
remove_fixtures(tc, &fixture);
|
||||
if (ue)
|
||||
test_ue_remove(ue);
|
||||
}
|
||||
|
||||
abts_suite *test_eir_registration(abts_suite *suite)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
suite = ADD_SUITE(suite)
|
||||
for (i = 0; i < OGS_ARRAY_SIZE(registration_cases); i++)
|
||||
abts_run_test(suite, registration_case, (void *)®istration_cases[i]);
|
||||
return suite;
|
||||
}
|
||||
340
tests/eir/service-test.c
Normal file
340
tests/eir/service-test.c
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
/*
|
||||
* Copyright (C) 2026 by Sukchan Lee <acetcom@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#include "integration.h"
|
||||
#include <curl/curl.h>
|
||||
|
||||
#define SERVICE_PEI "imeisv-4901542032375199"
|
||||
#define SERVICE_SUPI "imsi-001010000029999"
|
||||
#define EQUIPMENT_PATH "/n5g-eir-eic/v1/equipment-status"
|
||||
#define DISCOVERY_PATH "/nnrf-disc/v1/nf-instances?target-nf-type=5G_EIR" \
|
||||
"&requester-nf-type=AMF&service-names=n5g-eir-eic"
|
||||
|
||||
typedef struct http_result_s {
|
||||
char body[65536];
|
||||
size_t length;
|
||||
char content_type[128];
|
||||
long status;
|
||||
long version;
|
||||
CURLcode error;
|
||||
const char *stage;
|
||||
} http_result_t;
|
||||
|
||||
static size_t receive_body(char *data, size_t size, size_t count, void *arg)
|
||||
{
|
||||
http_result_t *result = arg;
|
||||
size_t bytes;
|
||||
|
||||
if (size && count > (sizeof(result->body) - 1 - result->length) / size) {
|
||||
ogs_debug("EIR HTTP response exceeds the test buffer [limit:%zu]",
|
||||
sizeof(result->body) - 1);
|
||||
return 0;
|
||||
}
|
||||
bytes = size * count;
|
||||
memcpy(result->body + result->length, data, bytes);
|
||||
result->length += bytes;
|
||||
result->body[result->length] = '\0';
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static bool http_request(const char *base, const char *path,
|
||||
const char *method, http_result_t *result)
|
||||
{
|
||||
CURL *curl;
|
||||
CURLcode rv;
|
||||
char *url, *content_type = NULL;
|
||||
|
||||
memset(result, 0, sizeof(*result));
|
||||
result->stage = "client initialization";
|
||||
curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
result->error = CURLE_FAILED_INIT;
|
||||
ogs_debug("Cannot initialize EIR test HTTP client");
|
||||
return false;
|
||||
}
|
||||
url = ogs_msprintf("%s%s", base, path);
|
||||
ogs_assert(url);
|
||||
|
||||
/* Exercise the real HTTP/2 listener; do not fall back to HTTP/1.1. */
|
||||
result->stage = "HTTP/2 setup";
|
||||
rv = curl_easy_setopt(curl, CURLOPT_HTTP_VERSION,
|
||||
CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE);
|
||||
if (rv != CURLE_OK) goto done;
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "AMF");
|
||||
curl_easy_setopt(curl, CURLOPT_PROXY, "");
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 500L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 2000L);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receive_body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, result);
|
||||
result->stage = "request";
|
||||
rv = curl_easy_perform(curl);
|
||||
if (rv == CURLE_OK) {
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result->status);
|
||||
curl_easy_getinfo(curl, CURLINFO_HTTP_VERSION, &result->version);
|
||||
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
|
||||
if (content_type)
|
||||
ogs_cpystrn(result->content_type, content_type,
|
||||
sizeof(result->content_type));
|
||||
}
|
||||
done:
|
||||
result->error = rv;
|
||||
if (rv != CURLE_OK)
|
||||
ogs_debug("EIR HTTP %s failed [%s:%s]", method,
|
||||
result->stage, curl_easy_strerror(rv));
|
||||
else if (result->version != CURL_HTTP_VERSION_2_0)
|
||||
ogs_debug("EIR test requires HTTP/2 [version:%ld]", result->version);
|
||||
ogs_free(url);
|
||||
curl_easy_cleanup(curl);
|
||||
return rv == CURLE_OK && result->version == CURL_HTTP_VERSION_2_0;
|
||||
}
|
||||
|
||||
static void log_http_failure(const char *name, const http_result_t *result)
|
||||
{
|
||||
ogs_error("EIR HTTP/2 test [%s] failed [%s:%s,http:%ld,version:%ld]",
|
||||
name, result->stage, curl_easy_strerror(result->error),
|
||||
result->status, result->version);
|
||||
}
|
||||
|
||||
static bool string_is(const cJSON *object, const char *key, const char *value)
|
||||
{
|
||||
const cJSON *item = cJSON_GetObjectItemCaseSensitive(object, key);
|
||||
return cJSON_IsString(item) && !strcmp(item->valuestring, value);
|
||||
}
|
||||
|
||||
static bool has_eir_service(const cJSON *profile)
|
||||
{
|
||||
const cJSON *services, *service;
|
||||
|
||||
services = cJSON_GetObjectItemCaseSensitive(profile, "nfServices");
|
||||
if (!services)
|
||||
services = cJSON_GetObjectItemCaseSensitive(profile, "nfServiceList");
|
||||
cJSON_ArrayForEach(service, services) {
|
||||
if (string_is(service, "serviceName", "n5g-eir-eic") &&
|
||||
string_is(service, "nfServiceStatus", "REGISTERED"))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool eir_registered(const http_result_t *result)
|
||||
{
|
||||
cJSON *document, *profiles, *profile;
|
||||
bool found = false;
|
||||
|
||||
if (result->status != 200) {
|
||||
ogs_debug("EIR NRF discovery returned HTTP status [%ld]",
|
||||
result->status);
|
||||
return false;
|
||||
}
|
||||
document = cJSON_Parse(result->body);
|
||||
if (!document) {
|
||||
ogs_debug("EIR NRF discovery returned invalid JSON");
|
||||
return false;
|
||||
}
|
||||
profiles = cJSON_GetObjectItemCaseSensitive(document, "nfInstances");
|
||||
cJSON_ArrayForEach(profile, profiles) {
|
||||
if (string_is(profile, "nfType", "5G_EIR") &&
|
||||
string_is(profile, "nfStatus", "REGISTERED") &&
|
||||
has_eir_service(profile)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(document);
|
||||
if (!found)
|
||||
ogs_debug("NRF discovery has no registered 5G-EIR service yet");
|
||||
return found;
|
||||
}
|
||||
|
||||
bool test_eir_wait_ready(void)
|
||||
{
|
||||
http_result_t result;
|
||||
ogs_time_t deadline = ogs_get_monotonic_time() + ogs_time_from_sec(20);
|
||||
|
||||
do {
|
||||
if (http_request(test_eir_nrf_uri(), DISCOVERY_PATH, "GET", &result) &&
|
||||
eir_registered(&result) &&
|
||||
http_request(test_eir_sbi_uri(),
|
||||
EQUIPMENT_PATH "?pei=" SERVICE_PEI, "GET", &result) &&
|
||||
(result.status == 200 || result.status == 404))
|
||||
return true;
|
||||
ogs_msleep(100);
|
||||
} while (ogs_get_monotonic_time() < deadline);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void discovery(abts_case *tc, void *data)
|
||||
{
|
||||
http_result_t result;
|
||||
bool received = http_request(test_eir_nrf_uri(),
|
||||
DISCOVERY_PATH, "GET", &result);
|
||||
bool registered;
|
||||
|
||||
ABTS_TRUE(tc, received);
|
||||
if (!received) {
|
||||
log_http_failure("NRF discovery", &result);
|
||||
return;
|
||||
}
|
||||
ABTS_INT_EQUAL(tc, 200, result.status);
|
||||
registered = eir_registered(&result);
|
||||
if (!registered)
|
||||
ogs_error("EIR discovery test found no registered service [http:%ld]",
|
||||
result.status);
|
||||
ABTS_TRUE(tc, registered);
|
||||
}
|
||||
|
||||
typedef struct service_case_s {
|
||||
const char *name;
|
||||
const char *path;
|
||||
const char *method;
|
||||
const char *record_status; /* NULL: no fixture */
|
||||
int http_status;
|
||||
const char *equipment_status;
|
||||
} service_case_t;
|
||||
|
||||
static const service_case_t cases[] = {
|
||||
{"whitelisted", EQUIPMENT_PATH "?pei=" SERVICE_PEI, "GET",
|
||||
"WHITELISTED", 200, "WHITELISTED"},
|
||||
{"blacklisted", EQUIPMENT_PATH "?pei=" SERVICE_PEI, "GET",
|
||||
"BLACKLISTED", 200, "BLACKLISTED"},
|
||||
{"greylisted", EQUIPMENT_PATH "?pei=" SERVICE_PEI, "GET",
|
||||
"GREYLISTED", 200, "GREYLISTED"},
|
||||
{"generic record with SUPI", EQUIPMENT_PATH "?pei=" SERVICE_PEI
|
||||
"&supi=" SERVICE_SUPI, "GET", "WHITELISTED", 200, "WHITELISTED"},
|
||||
{"unknown equipment", EQUIPMENT_PATH "?pei=" SERVICE_PEI, "GET",
|
||||
NULL, 404, NULL},
|
||||
{"missing PEI", EQUIPMENT_PATH, "GET", NULL, 400, NULL},
|
||||
{"invalid PEI", EQUIPMENT_PATH "?pei=imeisv-invalid", "GET",
|
||||
NULL, 400, NULL},
|
||||
{"invalid SUPI", EQUIPMENT_PATH "?pei=" SERVICE_PEI "&supi=imsi-x",
|
||||
"GET", "WHITELISTED", 400, NULL},
|
||||
{"IMSI below EIR minimum", EQUIPMENT_PATH "?pei=" SERVICE_PEI
|
||||
"&supi=imsi-00101", "GET", "WHITELISTED", 400, NULL},
|
||||
{"IMSI at EIR minimum", EQUIPMENT_PATH "?pei=" SERVICE_PEI
|
||||
"&supi=imsi-001010", "GET", "WHITELISTED", 200, "WHITELISTED"},
|
||||
{"invalid database status", EQUIPMENT_PATH "?pei=" SERVICE_PEI,
|
||||
"GET", "INVALID", 500, NULL},
|
||||
{"unsupported method", EQUIPMENT_PATH "?pei=" SERVICE_PEI,
|
||||
"POST", NULL, 403, NULL},
|
||||
{"unsupported version", "/n5g-eir-eic/v2/equipment-status?pei=" SERVICE_PEI,
|
||||
"GET", NULL, 400, NULL},
|
||||
{"unknown resource", "/n5g-eir-eic/v1/unknown?pei=" SERVICE_PEI,
|
||||
"GET", NULL, 400, NULL},
|
||||
};
|
||||
|
||||
static void service_request(abts_case *tc, void *data)
|
||||
{
|
||||
const service_case_t *test = data;
|
||||
mongoc_collection_t *collection = ogs_mongoc()->collection.eir;
|
||||
bson_t query, record, owned;
|
||||
bson_t *insert_opts = NULL;
|
||||
bson_oid_t oid;
|
||||
bson_error_t error;
|
||||
int64_t count;
|
||||
bool inserted = false, received;
|
||||
http_result_t result;
|
||||
const char *expected_content_type;
|
||||
cJSON *document, *status;
|
||||
|
||||
ogs_debug("EIR HTTP/2: %s", test->name);
|
||||
bson_init(&query);
|
||||
BSON_APPEND_UTF8(&query, "pei", SERVICE_PEI);
|
||||
count = mongoc_collection_count_documents(
|
||||
collection, &query, NULL, NULL, NULL, &error);
|
||||
bson_destroy(&query);
|
||||
if (count != 0) {
|
||||
ABTS_FAIL(tc, count < 0 ? error.message :
|
||||
"EIR HTTP fixture PEI already exists; existing records kept");
|
||||
return;
|
||||
}
|
||||
|
||||
bson_oid_init(&oid, NULL);
|
||||
bson_init(&owned);
|
||||
BSON_APPEND_OID(&owned, "_id", &oid);
|
||||
if (test->record_status) {
|
||||
bson_init(&record);
|
||||
BSON_APPEND_OID(&record, "_id", &oid);
|
||||
BSON_APPEND_UTF8(&record, "pei", SERVICE_PEI);
|
||||
BSON_APPEND_UTF8(&record, "status", test->record_status);
|
||||
/* Insert the malformed fixture even when the shared collection has
|
||||
* a validator. The exception applies only to this test-owned document;
|
||||
* normal fixtures still use the collection's validation rules. */
|
||||
if (!strcmp(test->record_status, "INVALID")) {
|
||||
insert_opts = BCON_NEW("bypassDocumentValidation", BCON_BOOL(true));
|
||||
ogs_assert(insert_opts);
|
||||
}
|
||||
inserted = mongoc_collection_insert_one(collection,
|
||||
&record, insert_opts, NULL, &error);
|
||||
if (insert_opts)
|
||||
bson_destroy(insert_opts);
|
||||
bson_destroy(&record);
|
||||
if (!inserted) {
|
||||
ABTS_FAIL(tc, error.message);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
received = http_request(test_eir_sbi_uri(),
|
||||
test->path, test->method, &result);
|
||||
ABTS_TRUE(tc, received);
|
||||
if (!received) {
|
||||
log_http_failure(test->name, &result);
|
||||
goto cleanup;
|
||||
}
|
||||
if (result.status != test->http_status)
|
||||
ogs_error("EIR HTTP/2 [%s] status mismatch [expected:%d,actual:%ld]",
|
||||
test->name, test->http_status, result.status);
|
||||
ABTS_INT_EQUAL(tc, test->http_status, result.status);
|
||||
expected_content_type = test->http_status == 200 ?
|
||||
OGS_SBI_CONTENT_JSON_TYPE : OGS_SBI_CONTENT_PROBLEM_TYPE;
|
||||
if (strncmp(result.content_type,
|
||||
expected_content_type, strlen(expected_content_type)))
|
||||
ogs_error("EIR HTTP/2 [%s] content type mismatch "
|
||||
"[expected:%s,actual:%s]", test->name,
|
||||
expected_content_type, result.content_type);
|
||||
ABTS_TRUE(tc, !strncmp(result.content_type,
|
||||
expected_content_type, strlen(expected_content_type)));
|
||||
document = cJSON_Parse(result.body);
|
||||
if (!document)
|
||||
ogs_error("EIR HTTP/2 [%s] returned invalid JSON", test->name);
|
||||
ABTS_PTR_NOTNULL(tc, document);
|
||||
if (document) {
|
||||
status = cJSON_GetObjectItemCaseSensitive(document, "status");
|
||||
if (test->equipment_status) {
|
||||
ABTS_TRUE(tc, cJSON_IsString(status));
|
||||
if (cJSON_IsString(status))
|
||||
ABTS_STR_EQUAL(tc, test->equipment_status, status->valuestring);
|
||||
} else {
|
||||
ABTS_TRUE(tc, cJSON_IsNumber(status));
|
||||
if (cJSON_IsNumber(status))
|
||||
ABTS_INT_EQUAL(tc, test->http_status, status->valueint);
|
||||
if (test->http_status == 404)
|
||||
ABTS_TRUE(tc, string_is(document,
|
||||
"cause", "ERROR_EQUIPMENT_UNKNOWN"));
|
||||
}
|
||||
cJSON_Delete(document);
|
||||
}
|
||||
cleanup:
|
||||
/* Delete only this case's generated _id, including uncertain inserts. */
|
||||
if (test->record_status &&
|
||||
!mongoc_collection_delete_one(collection,
|
||||
&owned, NULL, NULL, &error))
|
||||
ABTS_FAIL(tc, error.message);
|
||||
bson_destroy(&owned);
|
||||
}
|
||||
|
||||
abts_suite *test_eir_service(abts_suite *suite)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
suite = ADD_SUITE(suite)
|
||||
abts_run_test(suite, discovery, NULL);
|
||||
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++)
|
||||
abts_run_test(suite, service_request, (void *)&cases[i]);
|
||||
return suite;
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ subdir('unit')
|
|||
subdir('af')
|
||||
subdir('common')
|
||||
subdir('app')
|
||||
subdir('eir')
|
||||
subdir('registration')
|
||||
subdir('vonr')
|
||||
subdir('slice')
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ abts_suite *test_ue_context(abts_suite *suite);
|
|||
abts_suite *test_reset(abts_suite *suite);
|
||||
abts_suite *test_multi_ue(abts_suite *suite);
|
||||
abts_suite *test_crash(abts_suite *suite);
|
||||
abts_suite *test_eir(abts_suite *suite);
|
||||
|
||||
const struct testlist {
|
||||
abts_suite *(*func)(abts_suite *suite);
|
||||
|
|
@ -47,7 +46,6 @@ const struct testlist {
|
|||
{test_ue_context},
|
||||
{test_reset},
|
||||
{test_multi_ue},
|
||||
{test_eir},
|
||||
#if 0 /* Since there is error LOG, we disabled the following test */
|
||||
{test_crash},
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,224 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2026 by Erol Yağız Aydın <ygzaydns@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "test-common.h"
|
||||
|
||||
#define TEST_EIR_SUPI "imsi-001011234567895"
|
||||
#define TEST_EIR_OTHER_SUPI "imsi-001019999999999"
|
||||
#define TEST_EIR_PEI "imeisv-4901542032375186"
|
||||
#define TEST_EIR_UNKNOWN_PEI "imeisv-9999999999999999"
|
||||
|
||||
static void generic_whitelist_and_pair_blacklist_func(
|
||||
abts_case *tc, void *data)
|
||||
{
|
||||
int rv;
|
||||
ogs_dbi_eir_record_t record;
|
||||
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, NULL, "WHITELISTED");
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, TEST_EIR_SUPI, "BLACKLISTED");
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
|
||||
/* Pair-specific record overrides the generic PEI record */
|
||||
rv = ogs_dbi_eir_check_equipment(TEST_EIR_SUPI, TEST_EIR_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_BLACKLISTED, record.status);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
/* A different SUPI using the same PEI falls back to the generic record */
|
||||
rv = ogs_dbi_eir_check_equipment(
|
||||
TEST_EIR_OTHER_SUPI, TEST_EIR_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_WHITELISTED, record.status);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
/* PEI-only lookup (no SUPI given) also resolves to the generic record */
|
||||
rv = ogs_dbi_eir_check_equipment(NULL, TEST_EIR_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_WHITELISTED, record.status);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
rv = test_db_remove_eir(TEST_EIR_PEI, TEST_EIR_SUPI);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
rv = test_db_remove_eir(TEST_EIR_PEI, NULL);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
}
|
||||
|
||||
static void pair_whitelist_overrides_generic_blacklist_func(
|
||||
abts_case *tc, void *data)
|
||||
{
|
||||
int rv;
|
||||
ogs_dbi_eir_record_t record;
|
||||
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, NULL, "BLACKLISTED");
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, TEST_EIR_SUPI, "WHITELISTED");
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
|
||||
rv = ogs_dbi_eir_check_equipment(TEST_EIR_SUPI, TEST_EIR_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_WHITELISTED, record.status);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
rv = ogs_dbi_eir_check_equipment(
|
||||
TEST_EIR_OTHER_SUPI, TEST_EIR_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_BLACKLISTED, record.status);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
rv = test_db_remove_eir(TEST_EIR_PEI, TEST_EIR_SUPI);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
rv = test_db_remove_eir(TEST_EIR_PEI, NULL);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
}
|
||||
|
||||
static void greylisted_func(abts_case *tc, void *data)
|
||||
{
|
||||
int rv;
|
||||
ogs_dbi_eir_record_t record;
|
||||
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, NULL, "GREYLISTED");
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
|
||||
rv = ogs_dbi_eir_check_equipment(TEST_EIR_SUPI, TEST_EIR_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_GREYLISTED, record.status);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
rv = test_db_remove_eir(TEST_EIR_PEI, NULL);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
}
|
||||
|
||||
static void unknown_equipment_func(abts_case *tc, void *data)
|
||||
{
|
||||
int rv;
|
||||
ogs_dbi_eir_record_t record;
|
||||
|
||||
rv = ogs_dbi_eir_check_equipment(
|
||||
TEST_EIR_SUPI, TEST_EIR_UNKNOWN_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_NOTFOUND, rv);
|
||||
}
|
||||
|
||||
static void invalid_status_func(abts_case *tc, void *data)
|
||||
{
|
||||
int rv;
|
||||
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, NULL, "BOGUS_STATUS");
|
||||
ABTS_INT_EQUAL(tc, OGS_ERROR, rv);
|
||||
|
||||
rv = test_db_remove_eir(TEST_EIR_PEI, NULL);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
}
|
||||
|
||||
static void pei_format_validation_func(abts_case *tc, void *data)
|
||||
{
|
||||
/* IMEI: 15 digits */
|
||||
ABTS_TRUE(tc, ogs_dbi_eir_pei_is_valid("imei-490154203237518"));
|
||||
/* IMEISV: 16 digits */
|
||||
ABTS_TRUE(tc, ogs_dbi_eir_pei_is_valid("imeisv-4901542032375186"));
|
||||
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_pei_is_valid(NULL));
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_pei_is_valid(""));
|
||||
/* wrong prefix */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_pei_is_valid("imsi-490154203237518"));
|
||||
/* IMEI one digit short */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_pei_is_valid("imei-49015420323751"));
|
||||
/* IMEI one digit too long */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_pei_is_valid("imei-4901542032375186"));
|
||||
/* IMEISV wrong length */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_pei_is_valid("imeisv-490154203237518"));
|
||||
/* non-digit characters */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_pei_is_valid("imei-49015420323751a"));
|
||||
/* extra '-' component */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_pei_is_valid("imei-490154203237518-x"));
|
||||
}
|
||||
|
||||
static void supi_format_validation_func(abts_case *tc, void *data)
|
||||
{
|
||||
ABTS_TRUE(tc, ogs_dbi_eir_supi_is_valid("imsi-001011234567895"));
|
||||
/* minimum-length IMSI (6 digits) */
|
||||
ABTS_TRUE(tc, ogs_dbi_eir_supi_is_valid("imsi-123456"));
|
||||
/* maximum-length IMSI (15 digits) */
|
||||
ABTS_TRUE(tc, ogs_dbi_eir_supi_is_valid("imsi-123456789012345"));
|
||||
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_supi_is_valid(NULL));
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_supi_is_valid(""));
|
||||
/* wrong prefix */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_supi_is_valid("imei-001011234567895"));
|
||||
/* too short */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_supi_is_valid("imsi-12345"));
|
||||
/* too long */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_supi_is_valid("imsi-1234567890123456"));
|
||||
/* non-digit characters */
|
||||
ABTS_TRUE(tc, !ogs_dbi_eir_supi_is_valid("imsi-00101123456789a"));
|
||||
}
|
||||
|
||||
static void generic_record_supi_null_equals_missing_func(
|
||||
abts_case *tc, void *data)
|
||||
{
|
||||
int rv;
|
||||
ogs_dbi_eir_record_t record;
|
||||
|
||||
/* Inserted with supi omitted, per test_db_insert_eir(pei, NULL, ...) */
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, NULL, "WHITELISTED");
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
|
||||
/* Generic PEI-only lookup must still find it (supi: {$eq: null} also
|
||||
* matches a document where supi is missing entirely). */
|
||||
rv = ogs_dbi_eir_check_equipment(NULL, TEST_EIR_PEI, &record);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_DBI_EIR_STATUS_WHITELISTED, record.status);
|
||||
ogs_dbi_eir_record_free(&record);
|
||||
|
||||
rv = test_db_remove_eir(TEST_EIR_PEI, NULL);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
}
|
||||
|
||||
static void ambiguous_duplicate_func(abts_case *tc, void *data)
|
||||
{
|
||||
int rv;
|
||||
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, NULL, "WHITELISTED");
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
rv = test_db_insert_eir(TEST_EIR_PEI, NULL, "BLACKLISTED");
|
||||
ABTS_INT_EQUAL(tc, OGS_ERROR, rv);
|
||||
|
||||
rv = test_db_remove_eir(TEST_EIR_PEI, NULL);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
}
|
||||
|
||||
abts_suite *test_eir(abts_suite *suite)
|
||||
{
|
||||
suite = ADD_SUITE(suite)
|
||||
|
||||
abts_run_test(suite, generic_whitelist_and_pair_blacklist_func, NULL);
|
||||
abts_run_test(suite,
|
||||
pair_whitelist_overrides_generic_blacklist_func, NULL);
|
||||
abts_run_test(suite, greylisted_func, NULL);
|
||||
abts_run_test(suite, unknown_equipment_func, NULL);
|
||||
abts_run_test(suite, invalid_status_func, NULL);
|
||||
abts_run_test(suite, pei_format_validation_func, NULL);
|
||||
abts_run_test(suite, supi_format_validation_func, NULL);
|
||||
abts_run_test(suite, generic_record_supi_null_equals_missing_func, NULL);
|
||||
abts_run_test(suite, ambiguous_duplicate_func, NULL);
|
||||
|
||||
return suite;
|
||||
}
|
||||
|
|
@ -29,7 +29,6 @@ test5gc_registration_sources = files('''
|
|||
reset-test.c
|
||||
multi-ue-test.c
|
||||
crash-test.c
|
||||
eir-test.c
|
||||
'''.split())
|
||||
|
||||
test5gc_registration_exe = executable('registration',
|
||||
|
|
|
|||
|
|
@ -75,12 +75,108 @@ static void proto_message_test2(abts_case *tc, void *data)
|
|||
ogs_free(full_dnn);
|
||||
}
|
||||
|
||||
typedef struct identity_case_s {
|
||||
const char *value;
|
||||
const char *type;
|
||||
int min_len;
|
||||
int max_len;
|
||||
bool valid;
|
||||
} identity_case_t;
|
||||
|
||||
#define IMEI_CASE(value, valid) \
|
||||
{value, OGS_ID_PEI_TYPE_IMEI, \
|
||||
OGS_MAX_IMEI_BCD_LEN, OGS_MAX_IMEI_BCD_LEN, valid}
|
||||
#define IMEISV_CASE(value, valid) \
|
||||
{value, OGS_ID_SUPI_TYPE_IMEISV, \
|
||||
OGS_MAX_IMEISV_BCD_LEN, OGS_MAX_IMEISV_BCD_LEN, valid}
|
||||
#define IMSI_CASE(value, minimum, valid) \
|
||||
{value, OGS_ID_SUPI_TYPE_IMSI, minimum, OGS_MAX_IMSI_BCD_LEN, valid}
|
||||
|
||||
static const identity_case_t identity_cases[] = {
|
||||
IMEI_CASE("imei-490154203237518", true),
|
||||
IMEISV_CASE("imeisv-4901542032375186", true),
|
||||
IMEI_CASE(NULL, false),
|
||||
IMEI_CASE("", false),
|
||||
IMEI_CASE("imei", false),
|
||||
IMEI_CASE("imei-", false),
|
||||
IMEI_CASE("imei-49015420323751", false),
|
||||
IMEI_CASE("imei-4901542032375186", false),
|
||||
IMEISV_CASE("imeisv-490154203237518", false),
|
||||
IMEISV_CASE("imeisv-49015420323751860", false),
|
||||
IMEI_CASE("imei-49015420323751x", false),
|
||||
IMEISV_CASE("imeisv-490154203237518x", false),
|
||||
IMEI_CASE("imsi-490154203237518", false),
|
||||
IMEI_CASE("IMEI-490154203237518", false),
|
||||
IMEI_CASE("imei-490154203237518-1", false),
|
||||
IMEI_CASE(" imei-490154203237518", false),
|
||||
IMEI_CASE("imei-490154203237518 ", false),
|
||||
IMEI_CASE("imei-+90154203237518", false),
|
||||
IMSI_CASE("imsi-00101", 6, false),
|
||||
IMSI_CASE("imsi-001010", 6, true),
|
||||
IMSI_CASE("imsi-001010123456789", 6, true),
|
||||
IMSI_CASE(NULL, 6, false),
|
||||
IMSI_CASE("", 6, false),
|
||||
IMSI_CASE("imsi", 6, false),
|
||||
IMSI_CASE("imsi-", 6, false),
|
||||
IMSI_CASE("imsi-0010", 6, false),
|
||||
IMSI_CASE("imsi-0010101234567890", 6, false),
|
||||
IMSI_CASE("imsi-00101012345678x", 6, false),
|
||||
IMSI_CASE("nsi-001010123456789", 6, false),
|
||||
IMSI_CASE("IMSI-001010123456789", 6, false),
|
||||
IMSI_CASE("imsi-001010123456789-1", 6, false),
|
||||
IMSI_CASE(" imsi-001010123456789", 6, false),
|
||||
IMSI_CASE("imsi-001010123456789 ", 6, false),
|
||||
/* Bounds belong to the caller, not a universal SUPI validation rule. */
|
||||
IMSI_CASE("imsi-00101", 5, true),
|
||||
IMSI_CASE("imsi-1", 1, true),
|
||||
IMSI_CASE("imsi-\xff", 1, false),
|
||||
{"custom-001", "custom", 1, 3, true},
|
||||
{"custom-0001", "custom", 1, 3, false},
|
||||
};
|
||||
|
||||
static void identity_format(abts_case *tc, void *data)
|
||||
{
|
||||
const identity_case_t *test = data;
|
||||
|
||||
ABTS_ASSERT(tc, test->value ? test->value : "NULL identity",
|
||||
ogs_id_bcd_is_valid(test->value, test->type,
|
||||
test->min_len, test->max_len) == test->valid);
|
||||
}
|
||||
|
||||
static void legacy_identity_bounds(abts_case *tc, void *data)
|
||||
{
|
||||
/* Keep the existing BCD APIs' nonempty, maximum-length-only contract. */
|
||||
ABTS_TRUE(tc, ogs_imsi_bcd_is_valid("1"));
|
||||
ABTS_TRUE(tc, ogs_imsi_bcd_is_valid("001010123456789"));
|
||||
ABTS_TRUE(tc, ogs_imeisv_bcd_is_valid("1"));
|
||||
ABTS_TRUE(tc, ogs_imeisv_bcd_is_valid("4901542032375186"));
|
||||
|
||||
/* All numeric APIs reject invalid input and log a warning. */
|
||||
ABTS_TRUE(tc, ogs_bcd_string_is_valid("001", 3));
|
||||
ABTS_TRUE(tc, !ogs_bcd_string_is_valid("", 3));
|
||||
ABTS_TRUE(tc, !ogs_bcd_string_is_valid("0001", 3));
|
||||
ABTS_TRUE(tc, !ogs_bcd_string_is_valid("+01", 3));
|
||||
ABTS_TRUE(tc, !ogs_bcd_string_is_valid("1 ", 3));
|
||||
ABTS_TRUE(tc, !ogs_bcd_string_is_valid("\xff", 3));
|
||||
ABTS_TRUE(tc, !ogs_imsi_bcd_is_valid(""));
|
||||
ABTS_TRUE(tc, !ogs_imsi_bcd_is_valid("0010101234567890"));
|
||||
ABTS_TRUE(tc, !ogs_imsi_bcd_is_valid("00101012345678x"));
|
||||
ABTS_TRUE(tc, !ogs_imeisv_bcd_is_valid(""));
|
||||
ABTS_TRUE(tc, !ogs_imeisv_bcd_is_valid("49015420323751860"));
|
||||
ABTS_TRUE(tc, !ogs_imeisv_bcd_is_valid("490154203237518x"));
|
||||
}
|
||||
|
||||
abts_suite *test_proto_message(abts_suite *suite)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
suite = ADD_SUITE(suite)
|
||||
|
||||
abts_run_test(suite, proto_message_test1, NULL);
|
||||
abts_run_test(suite, proto_message_test2, NULL);
|
||||
for (i = 0; i < OGS_ARRAY_SIZE(identity_cases); i++)
|
||||
abts_run_test(suite, identity_format, (void *)&identity_cases[i]);
|
||||
abts_run_test(suite, legacy_identity_bounds, NULL);
|
||||
|
||||
return suite;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -919,63 +919,169 @@ static void sbi_message_test10(abts_case *tc, void *param)
|
|||
}
|
||||
}
|
||||
|
||||
static void sbi_message_test11(abts_case *tc, void *data)
|
||||
typedef struct eir_status_case_s {
|
||||
OpenAPI_equipment_status_e value;
|
||||
const char *name;
|
||||
} eir_status_case_t;
|
||||
|
||||
static const eir_status_case_t eir_status_cases[] = {
|
||||
{OpenAPI_equipment_status_WHITELISTED, "WHITELISTED"},
|
||||
{OpenAPI_equipment_status_BLACKLISTED, "BLACKLISTED"},
|
||||
{OpenAPI_equipment_status_GREYLISTED, "GREYLISTED"},
|
||||
};
|
||||
|
||||
static void eir_query_roundtrip(abts_case *tc, void *data)
|
||||
{
|
||||
OpenAPI_eir_response_data_t *EirResponseData = NULL;
|
||||
OpenAPI_problem_details_t ProblemDetails;
|
||||
cJSON *item = NULL;
|
||||
const char *supi = data;
|
||||
ogs_sbi_request_t *request;
|
||||
ogs_sbi_message_t message;
|
||||
ogs_time_t duration;
|
||||
int rv;
|
||||
|
||||
ABTS_STR_EQUAL(tc, "WHITELISTED",
|
||||
OpenAPI_equipment_status_ToString(
|
||||
OpenAPI_equipment_status_WHITELISTED));
|
||||
ABTS_STR_EQUAL(tc, "BLACKLISTED",
|
||||
OpenAPI_equipment_status_ToString(
|
||||
OpenAPI_equipment_status_BLACKLISTED));
|
||||
ABTS_STR_EQUAL(tc, "GREYLISTED",
|
||||
OpenAPI_equipment_status_ToString(
|
||||
OpenAPI_equipment_status_GREYLISTED));
|
||||
memset(&message, 0, sizeof(message));
|
||||
message.h.method = (char *)OGS_SBI_HTTP_METHOD_GET;
|
||||
message.h.uri = (char *)"/n5g-eir-eic/v1/equipment-status";
|
||||
message.param.pei = (char *)"imeisv-4901542032375186";
|
||||
message.param.supi = (char *)supi;
|
||||
|
||||
ABTS_INT_EQUAL(tc, OpenAPI_equipment_status_WHITELISTED,
|
||||
OpenAPI_equipment_status_FromString("WHITELISTED"));
|
||||
ABTS_INT_EQUAL(tc, OpenAPI_equipment_status_BLACKLISTED,
|
||||
OpenAPI_equipment_status_FromString("BLACKLISTED"));
|
||||
ABTS_INT_EQUAL(tc, OpenAPI_equipment_status_GREYLISTED,
|
||||
OpenAPI_equipment_status_FromString("GREYLISTED"));
|
||||
ABTS_INT_EQUAL(tc, OpenAPI_equipment_status_NULL,
|
||||
OpenAPI_equipment_status_FromString("BOGUS"));
|
||||
/* The request builder requires a timeout, without starting an NF. */
|
||||
duration = ogs_local_conf()->time.message.duration;
|
||||
ogs_local_conf()->time.message.duration = ogs_time_from_sec(10);
|
||||
request = ogs_sbi_build_request(&message);
|
||||
ogs_local_conf()->time.message.duration = duration;
|
||||
ABTS_PTR_NOTNULL(tc, request);
|
||||
if (!request) return;
|
||||
|
||||
EirResponseData = OpenAPI_eir_response_data_create(
|
||||
OpenAPI_equipment_status_BLACKLISTED);
|
||||
ogs_assert(EirResponseData);
|
||||
rv = ogs_sbi_parse_request(&message, request);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_STR_EQUAL(tc, OGS_SBI_HTTP_METHOD_GET, message.h.method);
|
||||
ABTS_STR_EQUAL(tc, "n5g-eir-eic", message.h.service.name);
|
||||
ABTS_STR_EQUAL(tc, OGS_SBI_API_V1, message.h.api.version);
|
||||
ABTS_STR_EQUAL(tc, OGS_SBI_RESOURCE_NAME_EQUIPMENT_STATUS,
|
||||
message.h.resource.component[0]);
|
||||
ABTS_STR_EQUAL(tc, "imeisv-4901542032375186", message.param.pei);
|
||||
ABTS_STR_EQUAL(tc, supi, message.param.supi);
|
||||
|
||||
item = OpenAPI_eir_response_data_convertToJSON(EirResponseData);
|
||||
ogs_assert(item);
|
||||
OpenAPI_eir_response_data_free(EirResponseData);
|
||||
ogs_sbi_message_free(&message);
|
||||
ogs_sbi_request_free(request);
|
||||
}
|
||||
|
||||
EirResponseData = OpenAPI_eir_response_data_parseFromJSON(item);
|
||||
ogs_assert(EirResponseData);
|
||||
cJSON_Delete(item);
|
||||
static void eir_response_roundtrip(abts_case *tc, void *data)
|
||||
{
|
||||
const eir_status_case_t *test = data;
|
||||
OpenAPI_eir_response_data_t body;
|
||||
ogs_sbi_message_t message, parsed;
|
||||
ogs_sbi_response_t *response;
|
||||
int rv;
|
||||
|
||||
ABTS_INT_EQUAL(tc, OpenAPI_equipment_status_BLACKLISTED,
|
||||
EirResponseData->status);
|
||||
ABTS_STR_EQUAL(tc, test->name,
|
||||
OpenAPI_equipment_status_ToString(test->value));
|
||||
ABTS_INT_EQUAL(tc, test->value,
|
||||
OpenAPI_equipment_status_FromString((char *)test->name));
|
||||
|
||||
OpenAPI_eir_response_data_free(EirResponseData);
|
||||
memset(&body, 0, sizeof(body));
|
||||
body.status = test->value;
|
||||
memset(&message, 0, sizeof(message));
|
||||
message.EirResponseData = &body;
|
||||
|
||||
memset(&ProblemDetails, 0, sizeof(ProblemDetails));
|
||||
ProblemDetails.is_status = true;
|
||||
ProblemDetails.status = OGS_SBI_HTTP_STATUS_NOT_FOUND;
|
||||
ProblemDetails.cause = (char *)"ERROR_EQUIPMENT_UNKNOWN";
|
||||
response = ogs_sbi_build_response(&message, OGS_SBI_HTTP_STATUS_OK);
|
||||
ABTS_PTR_NOTNULL(tc, response);
|
||||
if (!response) return;
|
||||
|
||||
item = OpenAPI_problem_details_convertToJSON(&ProblemDetails);
|
||||
ogs_assert(item);
|
||||
/* The client transport normally copies the originating request header. */
|
||||
response->h.method = ogs_strdup(OGS_SBI_HTTP_METHOD_GET);
|
||||
response->h.uri = ogs_strdup("/n5g-eir-eic/v1/equipment-status");
|
||||
rv = ogs_sbi_parse_response(&parsed, response);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_SBI_HTTP_STATUS_OK, parsed.res_status);
|
||||
ABTS_STR_EQUAL(tc, OGS_SBI_CONTENT_JSON_TYPE, parsed.http.content_type);
|
||||
ABTS_PTR_NOTNULL(tc, parsed.EirResponseData);
|
||||
if (parsed.EirResponseData)
|
||||
ABTS_INT_EQUAL(tc, test->value, parsed.EirResponseData->status);
|
||||
|
||||
ABTS_STR_EQUAL(tc, "ERROR_EQUIPMENT_UNKNOWN",
|
||||
cJSON_GetObjectItemCaseSensitive(item, "cause")->valuestring);
|
||||
ABTS_INT_EQUAL(tc, OGS_SBI_HTTP_STATUS_NOT_FOUND,
|
||||
(int)cJSON_GetObjectItemCaseSensitive(item, "status")->valuedouble);
|
||||
ogs_sbi_message_free(&parsed);
|
||||
ogs_sbi_response_free(response);
|
||||
}
|
||||
|
||||
static void eir_malformed_response(abts_case *tc, void *data)
|
||||
{
|
||||
cJSON *item = cJSON_Parse(data);
|
||||
OpenAPI_eir_response_data_t *body;
|
||||
int id = ogs_log_get_domain_id("core");
|
||||
ogs_log_level_e level = ogs_log_get_domain_level(id);
|
||||
|
||||
ABTS_PTR_NOTNULL(tc, item);
|
||||
if (!item) return;
|
||||
|
||||
if (level >= OGS_LOG_ERROR && level < OGS_LOG_DEBUG)
|
||||
ogs_log_set_domain_level(id, OGS_LOG_FATAL);
|
||||
body = OpenAPI_eir_response_data_parseFromJSON(item);
|
||||
ogs_log_set_domain_level(id, level);
|
||||
ABTS_PTR_EQUAL(tc, NULL, body);
|
||||
|
||||
OpenAPI_eir_response_data_free(body);
|
||||
cJSON_Delete(item);
|
||||
}
|
||||
|
||||
static void eir_unknown_status(abts_case *tc, void *data)
|
||||
{
|
||||
cJSON *item = cJSON_Parse("{\"status\":\"BOGUS\"}");
|
||||
OpenAPI_eir_response_data_t *body;
|
||||
|
||||
ABTS_PTR_NOTNULL(tc, item);
|
||||
if (!item) return;
|
||||
|
||||
ABTS_INT_EQUAL(tc, OpenAPI_equipment_status_NULL,
|
||||
OpenAPI_equipment_status_FromString("BOGUS"));
|
||||
/* The generated model accepts unknown enum strings as status_NULL. */
|
||||
body = OpenAPI_eir_response_data_parseFromJSON(item);
|
||||
ABTS_PTR_NOTNULL(tc, body);
|
||||
if (body)
|
||||
ABTS_INT_EQUAL(tc, OpenAPI_equipment_status_NULL, body->status);
|
||||
|
||||
OpenAPI_eir_response_data_free(body);
|
||||
cJSON_Delete(item);
|
||||
}
|
||||
|
||||
static void eir_problem_roundtrip(abts_case *tc, void *data)
|
||||
{
|
||||
OpenAPI_problem_details_t problem;
|
||||
ogs_sbi_message_t message, parsed;
|
||||
ogs_sbi_response_t *response;
|
||||
int rv;
|
||||
|
||||
memset(&problem, 0, sizeof(problem));
|
||||
problem.is_status = true;
|
||||
problem.status = OGS_SBI_HTTP_STATUS_NOT_FOUND;
|
||||
problem.cause = (char *)"ERROR_EQUIPMENT_UNKNOWN";
|
||||
memset(&message, 0, sizeof(message));
|
||||
message.http.content_type = (char *)OGS_SBI_CONTENT_PROBLEM_TYPE;
|
||||
message.ProblemDetails = &problem;
|
||||
|
||||
response = ogs_sbi_build_response(&message, problem.status);
|
||||
ABTS_PTR_NOTNULL(tc, response);
|
||||
if (!response) return;
|
||||
|
||||
/* The client transport normally copies the originating request header. */
|
||||
response->h.method = ogs_strdup(OGS_SBI_HTTP_METHOD_GET);
|
||||
response->h.uri = ogs_strdup("/n5g-eir-eic/v1/equipment-status");
|
||||
rv = ogs_sbi_parse_response(&parsed, response);
|
||||
ABTS_INT_EQUAL(tc, OGS_OK, rv);
|
||||
ABTS_INT_EQUAL(tc, OGS_SBI_HTTP_STATUS_NOT_FOUND, parsed.res_status);
|
||||
ABTS_STR_EQUAL(tc, OGS_SBI_CONTENT_PROBLEM_TYPE, parsed.http.content_type);
|
||||
ABTS_PTR_NOTNULL(tc, parsed.ProblemDetails);
|
||||
if (parsed.ProblemDetails) {
|
||||
ABTS_TRUE(tc, parsed.ProblemDetails->is_status);
|
||||
ABTS_INT_EQUAL(tc, OGS_SBI_HTTP_STATUS_NOT_FOUND,
|
||||
parsed.ProblemDetails->status);
|
||||
ABTS_STR_EQUAL(tc, "ERROR_EQUIPMENT_UNKNOWN",
|
||||
parsed.ProblemDetails->cause);
|
||||
}
|
||||
|
||||
ogs_sbi_message_free(&parsed);
|
||||
ogs_sbi_response_free(response);
|
||||
}
|
||||
|
||||
/* oneOf union (x-open5gs-union): SmSubsData is a bare array or an
|
||||
* ExtendedSmSubsData object on the wire, never a wrapper object */
|
||||
static void sbi_message_test12(abts_case *tc, void *data)
|
||||
|
|
@ -1093,6 +1199,8 @@ static void sbi_message_test12(abts_case *tc, void *data)
|
|||
|
||||
abts_suite *test_sbi_message(abts_suite *suite)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
suite = ADD_SUITE(suite)
|
||||
|
||||
abts_run_test(suite, sbi_message_test1, NULL);
|
||||
|
|
@ -1105,7 +1213,17 @@ abts_suite *test_sbi_message(abts_suite *suite)
|
|||
abts_run_test(suite, sbi_message_test8, NULL);
|
||||
abts_run_test(suite, sbi_message_test9, NULL);
|
||||
abts_run_test(suite, sbi_message_test10, NULL);
|
||||
abts_run_test(suite, sbi_message_test11, NULL);
|
||||
abts_run_test(suite, eir_query_roundtrip,
|
||||
(void *)"imsi-001010123456789");
|
||||
abts_run_test(suite, eir_query_roundtrip, NULL);
|
||||
for (i = 0; i < OGS_ARRAY_SIZE(eir_status_cases); i++)
|
||||
abts_run_test(suite, eir_response_roundtrip,
|
||||
(void *)&eir_status_cases[i]);
|
||||
abts_run_test(suite, eir_malformed_response, (void *)"{}");
|
||||
abts_run_test(suite, eir_malformed_response, (void *)"{\"status\":1}");
|
||||
abts_run_test(suite, eir_malformed_response, (void *)"{\"status\":null}");
|
||||
abts_run_test(suite, eir_unknown_status, NULL);
|
||||
abts_run_test(suite, eir_problem_roundtrip, NULL);
|
||||
abts_run_test(suite, sbi_message_test12, NULL);
|
||||
|
||||
return suite;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue