Skip to main content

SMS Firewall Service — API Contracts

Version: 1.0 Status: Draft Owner: Trust & Safety Last Updated: 2026-04-21 Companion: SERVICE_OVERVIEW · APPLICATION_LOGIC · SECURITY_MODEL · SYNC_CONTRACT

The sms-firewall-service exposes three interface planes:

  1. gRPC data-plane on :50061 (mTLS required, SPIFFE) — invoked by smpp-connector-{mno}-{rx|trx} and smpp-connector-transit-rx. Hot path. P95 ≤ 30 ms (MO) / 50 ms (TRANSIT).
  2. gRPC control-plane on :50062 (mTLS required) — invoked by routing-engine (egress DND check), channel-router-service (verdict lookup), and other in-mesh services.
  3. HTTPS REST admin on :3061 — fronted by Kong (JWT + RBAC). For NOC console, T&S admin, and regulator auditor flows.

Both gRPC surfaces share proto/firewall/v1/firewall.proto. All REST responses use the canonical envelope.


1. gRPC service — SmsFirewallService.v1 (data-plane)

syntax = "proto3";
package ghasi.sms.firewall.v1;
option go_package = "github.com/ghasi/sms-gateway/firewall/v1";

service SmsFirewallService {
// Hot path. mTLS required. Caller SVID matched against
// spiffe://ghasi/np-data/smpp-connector-*. P95 ≤ 30 ms.
rpc FilterInbound(FilterInboundRequest) returns (Verdict);

// Hot path. mTLS required. Caller SVID matched against
// spiffe://ghasi/np-data/smpp-connector-transit-*. P95 ≤ 50 ms.
rpc EvaluateTransit(EvaluateTransitRequest) returns (Verdict);

// Idempotent verdict lookup (cdr-mediation, fraud-intel offline replay).
// Returns NOT_FOUND if outside cache TTL.
rpc GetVerdict(GetVerdictRequest) returns (Verdict);
}

message FilterInboundRequest {
string trace_id = 1;
string src_msisdn = 2; // E.164
string dst_msisdn = 3; // E.164
string mno_bind_id = 4; // e.g. "awcc-rx-01"
bytes pdu_body = 5; // raw SMPP TLV payload bytes
int32 pdu_coding = 6; // SMPP data_coding: 0 (GSM7) | 3 (latin1) | 8 (UCS2)
int32 pdu_ton = 7; // 0..6 per SMPP §5.2.5
int32 pdu_npi = 8; // 0..18 per SMPP §5.2.6
google.protobuf.Timestamp recv_ts = 9;
uint32 smpp_sequence_number = 10;
string sender_id = 11; // optional alphanumeric originator
}

message EvaluateTransitRequest {
string trace_id = 1;
uint32 peer_asn = 2;
string peer_system_id = 3;
string src_addr = 4;
string dst_msisdn = 5;
string sender_id = 6;
bytes pdu_body = 7;
int32 pdu_ton = 8;
int32 pdu_npi = 9;
bool registered_delivery = 10;
int32 esm_class = 11;
}

message GetVerdictRequest {
string verdict_id = 1;
}

message Verdict {
string verdict_id = 1; // UUIDv4, prefix "fv_"
string trace_id = 2;
FirewallAction verdict = 3;
FirewallDirection direction = 4;
BlockReason block_reason = 5; // unspecified for ALLOW/FLAG
string hold_id = 6; // populated for QUARANTINE
repeated RuleHit rule_hits = 7;
repeated string evaluated_rule_ids = 8;
int64 evaluation_latency_ms = 9;
uint32 effective_ttl_seconds = 10;
repeated string flags = 11; // MAINTENANCE_MODE, RATE_GOVERNOR_DEGRADED, ...
google.protobuf.Timestamp evaluated_at = 12;
}

enum FirewallAction {
FIREWALL_ACTION_UNSPECIFIED = 0;
ALLOW = 1;
FLAG = 2;
BLOCK = 3;
QUARANTINE = 4;
RATE_LIMIT = 5;
}

enum FirewallDirection {
FIREWALL_DIRECTION_UNSPECIFIED = 0;
MO = 1;
TRANSIT_MT = 2;
EGRESS_DND_CHECK = 3;
}

enum BlockReason {
BLOCK_REASON_UNSPECIFIED = 0;
ORIGIN_BLOCKLIST = 1;
CONTENT_FORBIDDEN = 2;
RATE_EXCEEDED = 3;
GEO_FORBIDDEN = 4;
DND_PRESENT = 5;
AIT_SIGNATURE = 6;
SIMBOX_SIGNATURE = 7;
REGULATOR_BLOCK = 8;
PEER_ASN_UNKNOWN = 9;
SENDER_ID_SPOOFED = 10;
SENDER_ID_SUSPENDED = 11;
GREY_ROUTE = 12;
PEER_QUARANTINED = 13;
}

message RuleHit {
string rule_id = 1;
string rule_name = 2;
string rule_type = 3;
FirewallAction action = 4;
string severity = 5;
string evidence = 6; // redacted; matched span replaced with ***
float confidence = 7;
}

gRPC error mapping

gRPC statusConditionCaller behaviour
OKVerdict returnedAct per verdict
INVALID_ARGUMENTMalformed E.164 / unknown enumConnector logs, drops PDU, alerts
FAILED_PRECONDITIONUnknown mno_bind_id/peer_asnSelf-deregister + alert
PERMISSION_DENIEDSVID not allowlistedCrash + PagerDuty
RESOURCE_EXHAUSTEDPer-pod concurrency cap (200/bind)Backoff and retry on sibling replica
UNAVAILABLE / DEADLINE_EXCEEDED (> 100ms)Pod downFail-closed: MO connector → local-disk WAL; transit connector → ESME_RSUBMITFAIL to peer
INTERNALHandler exceptionSame as UNAVAILABLE

Idempotency & per-bind concurrency

  • FilterInbound is keyed by (mno_bind_id, smpp_sequence_number) for in-flight dedup; same SMPP retry within 30 s returns the cached verdict.
  • Per-pod concurrency cap 200 in-flight per mno_bind_id (configurable; prevents one runaway bind from starving others).

2. gRPC service — FirewallControlPlane.v1

service FirewallControlPlane {
// Called by routing-engine before egress to MNO; checks national DND
// for tenant-originated MT only. Read-only verdict.
rpc CheckOutboundEgress(CheckOutboundEgressRequest)
returns (Verdict);

// Returns the rule-set version + checksum currently loaded.
// Used by ops tooling for cluster-wide consistency checks.
rpc GetActiveRuleSetVersion(google.protobuf.Empty)
returns (RuleSetVersionResponse);

// smpp-connector pods POST a heartbeat every 30 s to confirm liveness.
rpc RegisterBindHeartbeat(BindHeartbeatRequest)
returns (google.protobuf.Empty);
}

message CheckOutboundEgressRequest {
string trace_id = 1;
string route_id = 2;
string dst_msisdn = 3;
string sender_id = 4;
string tenant_id = 5;
}

message RuleSetVersionResponse {
uint64 version = 1;
string sha256 = 2;
uint32 active_rules = 3;
google.protobuf.Timestamp loaded_at = 4;
}

message BindHeartbeatRequest {
string mno_bind_id = 1;
string mno_id = 2;
string pod_name = 3;
google.protobuf.Timestamp ts = 4;
}

3. REST — public surface

Base path: /v1/admin/firewall (admin) and /v1/internal/firewall (mesh-internal). All endpoints fronted by Kong (jwt plugin + rate-limiting-advanced). All responses JSON. State-changing endpoints support Idempotency-Key header.

3.1 Rules CRUD (FW-US-016)

MethodPathRolePurpose
GET/v1/admin/firewall/rulestns-admin, tns-reader, regulator-auditorPaginated list with filters scope, enabled, type
GET/v1/admin/firewall/rules/{ruleId}sameCurrent rule
GET/v1/admin/firewall/rules/{ruleId}/versionssameVersion history
POST/v1/admin/firewall/rulestns-adminCreate. Body: {name, scope, type, expression, action, blockReasonCode, severity, priority, enabled} → 201 {ruleId, version: 1}
PUT/v1/admin/firewall/rules/{ruleId}tns-adminUpdate; new immutable version row created; version bumped
POST/v1/admin/firewall/rules/{ruleId}/enabletns-adminIdempotent enable
POST/v1/admin/firewall/rules/{ruleId}/disabletns-adminIdempotent disable
POST/v1/admin/firewall/rules/{ruleId}/testtns-adminDry-run against supplied MoContext / TransitMtContext; does NOT write audit
DELETE/v1/admin/firewall/rules/{ruleId}tns-adminSoft-delete

Create body example:

{
"name": "Block OTP harvest from non-AF source",
"scope": "MO",
"type": "CONTENT_REGEX",
"expression": "pdu.body.matches('(?i)\\b(verify|otp)\\b') && src.country != 'AF'",
"action": "BLOCK",
"blockReasonCode": "CONTENT_FORBIDDEN",
"severity": "HIGH",
"priority": 100,
"enabled": true
}

3.2 Blocklist CRUD (FW-US-012, FW-US-014, FW-US-015)

MethodPathRolePurpose
GET/v1/admin/firewall/blockliststns-admin, regulator-auditorList blocklists
GET/v1/admin/firewall/blocklists/{id}/entriessamePaginated entries (cursor-based)
POST/v1/admin/firewall/blocklists/{id}/entriestns-adminAdd entry; source defaults to OPERATOR_MANUAL
POST/v1/admin/firewall/blocklists/{id}/entries:bulktns-adminAtomic batch ≤ 10 000 entries; rejects partial
DELETE/v1/admin/firewall/blocklists/{id}/entries/{entryId}tns-adminSoft-delete
GET/v1/admin/firewall/blocklist/{entryId}/historytns-admin, regulator-auditorChronological events: created, source_added, source_removed, confidence_changed, manually_overridden, deactivated, reactivated
POST/v1/admin/firewall/federation/synctns-adminManually trigger federation export (out-of-band)

3.3 Quarantine (FW-US-011)

MethodPathRolePurpose
GET/v1/admin/firewall/quarantine?status=PENDING&page=1&pageSize=50noc, tns-adminPaginated list
GET/v1/admin/firewall/quarantine/{holdId}noc, tns-adminFull MoContext/TransitMtContext; PDU body decrypted on the fly (audit logged); body redacted in the response logger
POST/v1/admin/firewall/quarantine/{holdId}/releasenoc{notes} → re-evaluate against current rules; on ALLOW → re-inject via firewall.quarantine.released.v1
POST/v1/admin/firewall/quarantine/{holdId}/rejectnoc{reason} → terminal REJECTED

3.4 MNO Bind Registry (FW-US-017)

MethodPathRolePurpose
GET/v1/admin/firewall/mno-bindscarrier-relations, tns-adminList (no secrets)
POST/v1/admin/firewall/mno-bindscarrier-relationsBody: {mnoBindId, mnoId, direction, permittedCountryCodes[], permittedSenderIds[], notes} → 201
PATCH/v1/admin/firewall/mno-binds/{id}carrier-relationsUpdate permitted lists
DELETE/v1/admin/firewall/mno-binds/{id}carrier-relationsSoft delete + firewall.mno_bind.deactivated.v1
POST/v1/internal/firewall/mno-binds/{id}/heartbeatsmpp-connector (mTLS SVID)Connector heartbeat; missing > 60 s emits firewall.alert.bind.missing.v1

3.5 Operating Mode (FW-US-019)

MethodPathRolePurpose
GET/v1/admin/firewall/modeany authenticatedCurrent mode
POST/v1/admin/firewall/modenoc, tns-admin{targetMode, reason, secondApproverToken?} — dual-approval (60 s window)
GET/v1/admin/firewall/mode/historytns-admin, regulator-auditorAudit chain of mode changes

3.6 Audit & Statistics

MethodPathRolePurpose
GET/v1/admin/firewall/audittns-admin, regulator-auditorFilter by direction, verdict, srcMsisdn, dstMsisdn, mnoBindId, peerAsn, from, to, cursor pagination; results MSISDN-masked for regulator-auditor
GET/v1/internal/firewall/blocklist/export?since={iso8601}regulator-portal-service (mTLS SVID)JSON Lines + HSM signature for regulator submission
GET/v1/admin/firewall/stats/publicany authenticatedAggregate, non-PII stats: verdict distribution, blocklist size, federation lag

3.7 Internal / operational

MethodPathCallerPurpose
GET/health/live, /health/readyKubernetesLiveness / readiness
GET/metricsPrometheusScrape (port 9061)

4. REST error envelope

{
"error": {
"code": "FIREWALL_RULE_INVALID_INPUT_REF",
"message": "Rule expression references undefined input 'pdu.foo'",
"traceId": "00-abc...",
"details": { "field": "expression", "ref": "pdu.foo" }
}
}
HTTPCodeWhen
400FIREWALL_RULE_INVALID_INPUT_REFRule expression references undefined input
400FIREWALL_VALIDATION_FAILEDPayload invalid
401UNAUTHENTICATEDMissing/invalid JWT
403INSUFFICIENT_SCOPECaller role lacks required scope
404NOT_FOUNDRule / blocklist entry / quarantine hold not found
409CONFLICTHold already reviewed; rule version mismatch; mode-switch already in progress
412DUAL_APPROVAL_REQUIREDMode switch / quarantine release without second approver
422RULE_UNSAFE_EXPRESSIONCEL expression contains forbidden function (os.system, file IO, network)
422RULE_REGEX_REDOS_RISKRegex failed ReDoS screen (≤ 50 ms safe-eval test)
422BLOCKLIST_BULK_PARTIAL_FAILBulk insert had per-entry validation failures (rolled back)
422FEDERATION_SIGNATURE_INVALIDRegulator HSM signature did not validate
429RATE_LIMITEDKong rate-limit
500INTERNALUnhandled internal error
503DEPENDENCY_UNAVAILABLEPostgres / Redis / NATS unavailable; fail-closed in effect

Cross-reference: docs/standards/ERROR_CODES.md.


5. Pagination & filtering conventions

  • Cursor pagination for time-series endpoints (audit, quarantine, blocklist/{id}/entries): query ?cursor={opaque}&limit={1..100}; response contains nextCursor and total.
  • Page-based for small admin lists (rules, mno-binds): ?page=1&pageSize=50 (max pageSize=200).
  • Sort: ?sort=field:asc|desc, default evaluatedAt:desc for time-series.
  • Time filters: ISO 8601 with timezone (e.g. ?from=2026-04-21T00:00:00+04:30&to=2026-04-21T23:59:59+04:30).

6. Rate limits & quotas (Kong)

SurfaceLimit
Admin REST (per user, per role tns-admin)600 req/min
Admin REST (per user, role noc)1200 req/min
Admin bulk endpoints (blocklist bulk, federation sync)10 req/min per user
Audit query (regulator-auditor)60 req/min
Statistics public600 req/min per token
gRPC FilterInbound / EvaluateTransitNo Kong limit (mTLS only); per-pod concurrency 1000, per-bind 200

7. Versioning

  • gRPC: ghasi.sms.firewall.v1. Breaking change → v2 package in parallel with ≥ 90-day deprecation.
  • REST: /v1/admin/firewall/*. Additive changes are non-breaking.
  • schemaVersion field on events per EVENT_SCHEMAS §7.
  • OpenAPI 3.1 served at GET /v1/admin/firewall/openapi.json.
  • .proto files versioned in this repo; generated TypeScript + Go clients published to internal package registry on every deploy.
  • Pact contract tests verify connector ↔ firewall gRPC contract and admin-dashboard ↔ firewall REST contract on every CI run.