Final year project · Dublin City University · 2024—2025
5G SMS Firewall
Text messages still carry one-time passcodes, bank alerts and account recovery links, which makes SMS one of the most attacked surfaces in a mobile network. This is a firewall that inspects SMS inside a 5G core, blocks smishing in real time, and was built and measured against a working simulation of the network it would sit in.
- OMNeT++ / C++
- Flask
- DistilBERT
- PostgreSQL
- Google Cloud Run
- React
- Period
- 2024 — 2025
- Context
- Final year project, B.Sc. Computer Science
- Institution
- Dublin City University
- Deployment
- Google Cloud Run, driven by an OMNeT++ simulation
- 81.8/s
- Messages sustained
- 1,100 ms
- 95th percentile
- <100 ms
- Model inference
- 0%
- Failure rate
At eight Message Processor instances under the high-traffic profile, with no failed requests.
Worst-case latency at peak load, down from 2,800 ms on a single instance.
Average DistilBERT classification time returned from Cloud Run.
Across every load profile and service configuration tested.
01The problem
SMS is old, trusted, and still the easiest way in
Almost every account you own can be reached through a text message. One-time passcodes, delivery notifications, bank alerts, password resets — all of it arrives over a protocol designed in the 1980s, with no sender authentication and no built-in way to tell a bank apart from somebody pretending to be one.
Attackers know this. Smishing — phishing delivered by SMS — works because the message lands in the same thread as the real ones, on a device people trust without thinking. As 5G moves messaging onto IP-based infrastructure and raises the volume networks carry, that surface only grows.
The tools operators traditionally point at the problem are static: fixed pattern matches and hand-maintained blacklists. They stop yesterday’s campaign. They do not stop a message that has never been seen before, written by somebody who knows what the filters look for.
This project asks what a filter built for that problem looks like: one that inspects messages inside the 5G core, pairs cheap deterministic checks with a language model that understands phrasing, and still delivers a legitimate message quickly enough that nobody notices it was read.
Smishing
Phishing delivered by text, trading on the trust people place in their message inbox.
Urgency baiting
Messages engineered to force an immediate click: an account suspended, a payment failed, a delivery held.
SMS flooding
High-frequency bursts from a single sender, used for spam campaigns or to exhaust network resources.
Malicious URLs
Shortened or lookalike links leading to credential-harvesting pages.
- UE
- gNodeB
- AMF
- Firewall
- SMSF
- UDM
02Try it
Run a message through the pipeline
The firewall reaches its decision in five stages. The first three run in parallel and can stop a message outright; only what survives them is scored, and only a score high enough to be worth it reaches a language model. Everything below runs that real decision logic in your browser.
Samples
Sent in window: 0/8
Send repeatedly from one number to trip the flood rule.
Pick a sample or write your own message, then inspect it.
- 01—
Rule filtering
Blacklisted MSISDNs and blocked country or network prefixes, read from the cached rule set.
- 02—
Spam flood detection
Messages per sender inside a sliding window. Exceeding it starts a cooldown during which everything is dropped.
- 03—
URL safety lookup
Any URL in the payload is extracted and checked against Google Safe Browsing.
- 04—
Word scoring engine
Weighted keywords, plus penalties for long digit runs and unusually short messages.
- 05—
DistilBERT classifier
A DistilBERT model fine-tuned on the SMS Spam Collection dataset, reached only above the score threshold.
03Architecture
Three planes, joined by one HTTP call
The system splits into three planes that each scale, deploy and fail on their own. A simulated 5G core carries the traffic. A set of cloud microservices decides what to do with it. An operator-side dashboard defines the rules and watches the result.
The only coupling between the network and the decision engine is a single synchronous HTTP call. That was deliberate: the firewall module inside the network knows nothing about scoring, machine learning or databases. It asks a question and acts on the answer, which means any cloud service can be rewritten or redeployed without the network noticing.
Rules travel the other way, and never synchronously. An administrator edits them locally, syncs them to Cloud SQL through a middleware service, and they are exported as JSON to a storage bucket the Message Processor polls. Nothing in the message path ever waits on a database.
Simulated 5G core
01
Custom OMNeT++ modules in C++ and NED implementing the network functions an SMS actually traverses, plus an IPX interconnect so messages can cross operator boundaries.
- UE
- gNodeB
- AMF
- Firewall
- SMSF
- UDM
- IPX
Cloud decision plane
02
Flask services on Google Cloud Run: the Message Processor that makes every decision, the DistilBERT classifier it escalates to, and a middleware service that syncs rules and returns logs. State lives in Cloud SQL and a rule cache in Cloud Storage.
- MessageProcessor
- DistilBERT service
- RuleLogInterface
- Cloud SQL
- GCS rule cache
Operator admin plane
03
A containerised Django and React dashboard where rules are written and reviewed before going live, with Redis and Celery absorbing the log stream so ingestion never blocks the interface.
- Django
- React
- PostgreSQL
- Redis
- Celery

04The network
A 5G core built to be attacked
Testing a firewall needs traffic, and traffic needs a network. Rather than mock one, we built a working model of the 5G control plane in OMNeT++ — every module written from scratch in C++ and NED, following the function definitions in 3GPP TS 23.501 and TS 33.501.
Messages travel the real path. A handset attaches to the network, the routing tables record which base station and access function it sits behind, and an SMS then crosses the radio access network, the access function, the firewall, the SMS function and the data management function before coming back down to the recipient.
Two operators were modelled and connected through IPX interconnect nodes, so inter-operator delivery could be exercised alongside internal traffic — the case where a message arrives from a network you do not control, which is where most abuse originates.
Topology, routing and message schedules are all driven from CSV and .ini files. Adding twelve handsets, a second operator or a new spam pattern is a configuration change rather than a recompile.
- UE (Actor)
- A handset, parameterised with an IMSI, an MSISDN and the section of the radio network it is attached to. Sends and receives on a schedule read from file.
- gNodeB
- The 5G base station. Resolves whether a message arrived from a handset or from the core by gate name rather than index, so the topology can change without touching the code.
- AMF
- Access and mobility management. Looks up which base station the recipient sits behind and routes the message onward, or into the firewall for inspection.
- Firewall
- A proxy between the core and the cloud. Serialises the message to JSON, posts it over libcurl, and either forwards it to the SMS function or discards it.
- SMSF
- The SMS function. Delivers internally back through the access function, or hands the message to the IPX for inter-operator routing.
- UDM
- Unified data management. Holds subscriber records and confirms the routing path before final delivery.
- IPX
- The interconnect between operators. Relays messages across network boundaries and hands inbound traffic to the receiving operator’s firewall.


Getting C++ to talk to a cloud service
OMNeT++ has no HTTP client. The firewall module uses libcurl to make a synchronous POST to the Message Processor on Google Cloud, builds the JSON payload by hand, and parses the verdict out of the response before deciding whether to forward or discard. It is an unglamorous piece of glue, and it is the thing that turns a simulation into an end-to-end test of production services.
05Decision engine
Cheap checks first, the model only when it earns it
Every message that reaches the Message Processor has to be answered inside the same request. That budget shapes the whole design: expensive work has to be rare, and nothing that can run concurrently should run in sequence.
Three independent checks are dispatched together on a thread pool, and the first block wins — if a sender is already blacklisted there is no reason to wait for a URL lookup to finish. Only messages that survive all three are scored, and only messages scoring at or above the threshold are sent to the language model. In practice the great majority of traffic never touches it.
Enforcement is deliberately forgiving. A single violation does not blacklist anybody: strikes are counted by category, and only a sender who crosses the threshold is added to the automated blocklist — which is then written straight into the in-memory rule cache, so it takes effect on the next message rather than at the next refresh.
- Parallel filtering. Rule matching, flood detection and URL lookup run concurrently on a thread pool, returning as soon as any one of them says block.
- Word scoring. Weighted keywords, editable from the dashboard on a scale of one to ten, plus penalties for long digit runs and unusually short messages. Twenty points escalates.
- Escalation. A DistilBERT model fine-tuned on the SMS Spam Collection dataset, hosted as its own Flask service so it can scale, and fail, independently.
- Strike system. Violations counted per category. Crossing the threshold blacklists the sender and updates the live rule cache immediately.
- Rate limiting. A sliding per-sender window with a cooldown afterwards, held in memory behind thread-safe counters, with no external rate limiter in the request path.
- Rule cache. Rules pulled from a storage bucket at startup and refreshed every ten minutes, retaining the last known good set if a fetch fails.
- Non-blocking tails. Database writes and webhook dispatch are submitted to a thread pool after the verdict is returned, so a slow consumer cannot slow down message handling.
- Connection pooling. A shared pool of up to forty PostgreSQL connections, introduced after early builds exhausted the server by opening one per message.

06Results
What it does under load
Load testing used Locust against the deployed services, across two traffic profiles and several service configurations. The 95th percentile is reported rather than the mean: what matters is the worst message, not the average one.
50 concurrent users, spawning 10/s
Throughput
req/s · ↑
- ×120.020.0
- ×237.737.7
- ×459.259.2
- ×881.881.8
Message Processor instances
95th percentile latency
ms · ↓
- ×12,8002,800
- ×21,9001,900
- ×41,8001,800
- ×81,1001,100
Message Processor instances
| MP instances | AI service | Throughput (req/s) | p95 latency (ms) | Failures |
|---|---|---|---|---|
| ×1 | 1 × 2 vCPU | 20.0 | 2,800 | 0% |
| ×2 | 1 × 2 vCPU | 37.7 | 1,900 | 0% |
| ×4 | 2 × 4 vCPU | 59.2 | 1,800 | 0% |
| ×8 | 2 × 4 vCPU | 81.8 | 1,100 | 0% |
- The bottleneck was not the model. The assumption going in was that the classifier would be the constraint. Cloud metrics showed the opposite: the AI service never reached its instance quota even under bursts, while the Message Processor saturated. Ingest, rule evaluation and routing were the limit.
- Scaling was close to linear. Going from four to eight Message Processor instances, with the AI service left unchanged, raised throughput by 39% and cut 95th percentile latency by more than 35%.
- It stayed correct under stress. No failed requests in any configuration at either profile. Nothing was dropped because a downstream service was slow.
- Sizing advice falls out of it. Under load, adding Message Processor replicas returns more than adding model replicas — until the model itself saturates, which these tests never managed to provoke.
07Resilience
Every dependency is allowed to fail
A firewall that stops passing messages when a database is unreachable has turned itself into the outage. Each dependency was given a defined failure behaviour, and message delivery depends on none of them.
| If this fails | The system does this |
|---|---|
| Rule storage unavailable | Falls back to the last cached rule set held in memory. Processing continues unchanged; no rule is lost, only the refresh is delayed. |
| Cloud SQL unreachable | Decisions are made from the in-memory rules and returned normally. Log writes fail quietly and are recorded as errors rather than blocking the verdict. |
| AI classifier offline | The escalation step is skipped and the message is allowed through on its score alone. The event is logged so the gap is visible afterwards. |
| Webhook endpoint unreachable | Dispatch is fire-and-forget on a background thread. Delivery failures are logged; the dashboard falls behind, the firewall does not. |
| Sender floods the network | The sliding window blocks the offending sender at the firewall, before the message reaches the access, SMS or data management functions — keeping flood traffic out of the core entirely. |
08Operations
The part an operator actually uses
A firewall is only as good as the ability to see what it did and change what it does next. The admin panel is a containerised Django and React application: Django serves the APIs and holds the authoritative rule set, React renders it, and Redis and Celery sit between log ingestion and processing so a burst of traffic queues instead of blocking.
Rules are edited locally and reviewed before they go anywhere. A sync pushes the current set — including soft-deleted entries, so deletions are versioned rather than silently dropped — to Cloud SQL through the middleware service, and a separate deploy step reloads the Message Processor.
Logs arrive continuously by webhook, with a manual pull as a fallback if the stream is interrupted. Each entry records the stage that decided the outcome, the rule that was violated, the score and the model prediction, so any block can be traced back to the reason for it.



- Live log table. Every processed message with its stage, rule violation, score and model prediction, filterable by country, status, reason and rule.
- Processing stages. A per-message breakdown showing which component reached the decision and how it got there.
- Geographic view. Blocked volume by country on an interactive map, with the underlying logs one click away.
- Word analysis. Which terms appear in blocked traffic and the block-to-allow ratio for each — the feedback loop for tuning the weights.
- Rule management. Spam thresholds, weighted words and blocked country networks, with explicit sync and deploy actions.
- Trend charts. Volume by hour, block rate over time, score trends and rule-match frequency.
09Engineering
What broke, and what fixed it
The interesting part of a build is rarely the design. These are the problems that only appeared once the system was running, and what each one changed.
01
Database connections ran out
Problem
Early builds opened a new PostgreSQL connection for every message. Under load the server hit its connection limit and processing stalled entirely — timeouts, failures, no throughput.
Fix
A shared connection pool of up to forty reused connections. Exhaustion stopped being possible and behaviour under concurrent load became predictable.
02
Webhooks blocked the request path
Problem
Webhook delivery and database logging ran inside the message-handling thread. One slow external endpoint delayed every message queued behind it.
Fix
Both moved onto a thread pool and are dispatched after the verdict is returned. A failure is logged, and nothing in the message path waits on it.
03
BERT was too heavy to serve
Problem
The first classifier used bert-base-uncased. Its memory footprint and inference time made it unusable for real-time filtering on serverless infrastructure.
Fix
Switched to distilbert-base-uncased and capped input at 25 tokens, which suits SMS length. Inference dropped under 100 ms with classification quality holding up.
04
Rules were fetched per message
Problem
Every message triggered a database query for the current rule set. It added latency to every request and put load on the database in direct proportion to traffic.
Fix
Rules are exported to a storage bucket as JSON, loaded into memory at startup and refreshed every ten minutes. Database load stopped scaling with traffic.
05
One mistake got you blacklisted
Problem
Any single violation blacklisted the sender immediately. A false positive, or one careless message, permanently cut off a legitimate subscriber.
Fix
A strike system counting violations by category, blacklisting only on crossing a threshold. Enforcement stayed firm without being brittle.
06
Blocklist updates were invisible for ten minutes
Problem
A sender blacklisted by the strike system was written to the database, but the in-memory rule cache did not know until its next scheduled refresh. For up to ten minutes a blocked sender could keep sending.
Fix
Offences now write into the live cache as well as the database, closing the window between a violation and its enforcement.
07
The simulation broke whenever the topology changed
Problem
The access function and base station modules routed by hardcoded gate indices. Changing the number of handsets or base stations in the .ini file caused silent message drops or outright crashes.
Fix
Routing resolves gates by name rather than by position. The simulation scales to any topology without a code change, which is what made the larger multi-operator scenarios possible at all.
08
The simulator could not speak HTTP
Problem
OMNeT++ has no HTTP support, but the decision engine lived on Google Cloud behind a REST API. Without a bridge, the simulation could only ever test a stub.
Fix
A libcurl client inside the firewall module, constructing JSON by hand and parsing the verdict out of the response. The simulation drives the real deployed services.
10Next
What it does not do yet
The system met what it set out to do. These are the limits we would take on next, and they were written down as limits rather than discovered as surprises.
- Confidence, not just labels. The classifier returns spam or ham. Returning a confidence score and the tokens that drove it would let borderline messages be quarantined rather than decided.
- Versioned rule propagation. Replace the ten-minute refresh with versioned rule files, switching to a new version during a traffic lull rather than on a timer.
- Decaying strikes. Strikes are permanent until reset. Severity weighting and decay over time would model reputation better than a plain counter.
- Richer scoring. The scoring engine matches single words. N-grams or TF-IDF weighting would catch phrasing that individual keywords miss, without changing anything else in the pipeline.
- Retraining on live traffic. The model was fine-tuned once on a public dataset. Periodic retraining on observed traffic is the obvious next step, and the logging already captures what it would need.
- Automated integration tests. Unit and system tests are automated; integration testing across the deployed services was done by hand. Scripting it is straightforward and was left undone for time.
Stack
Built with
Simulation
- OMNeT++
- INET
- Simu5G
- C++
- NED
- libcurl
Services
- Python
- Flask
- Django
- React
- Celery
- Redis
Machine learning
- DistilBERT
- Hugging Face Transformers
- PyTorch
Infrastructure
- Google Cloud Run
- Cloud SQL
- Cloud Storage
- PostgreSQL
- Docker
- GitLab CI/CD
Credits
Team and documentation
Built as a two-person final year project for the B.Sc. in Computer Science at Dublin City University, worked on jointly across the simulation, the cloud services and the dashboard.
- Built with
- Jack Keenan
- Supervisor
- Prof. Mohammed Amine Togou
- Institution
- Dublin City University, 2024—2025
- Standards
- 3GPP TS 23.501, TS 24.501, TS 33.501
- Dataset
- SMS Spam Collection (Kaggle), used to fine-tune the classifier
- Documentation
- Functional specification, technical specification, testing document and user manual — available on request.