VectorCore TWAG is a Trusted WLAN Access Gateway implementation for lab and carrier-style 3GPP Wi-Fi access testing. It bridges WPA2/WPA3 Enterprise 802.1X access on the WLAN side to a PGW over GTP-C/GTP-U, using 3GPP AAA over Diameter STa/SWx-style EAP-AKA′ authentication.
The current implementation is focused on trusted non-3GPP WLAN access where the UE authenticates through an AP using RADIUS/EAP, the TWAG exchanges EAP-AKA′ with the AAA/HSS path, creates a PGW session, receives a subscriber IP address, and provides the UE with DHCP/ARP/access forwarding into the GTP user-plane path.
This project is under active development and lab validation.
Currently implemented or being validated:
- RADIUS server for WPA2/WPA3 Enterprise AP integration
- EAP-AKA′ authentication over Diameter STa
- RADIUS Access-Challenge / Access-Accept flow
- RADIUS Accounting listener and Accounting-Start/Stop correlation
- RADIUS authentication cache for cached 802.1X / PMK reconnect recovery
- MPPE key export for 802.1X
- RADIUS VLAN assignment
- PGW GTP-C Create Session / Delete Session
- Kernel GTP-U session installation using Linux
gtp0 - PGW-assigned subscriber IP handling
- Access-side DHCP for UE IP delivery
- ARP proxy for a configurable virtual gateway
- Access-side forwarding integration
- GTP-C transaction correlation
- GTP-C Echo watchdog
- PGW-initiated GTP-C Echo Request handling
- GTP-U Error Indication handling
- Session recovery tombstones
- RADIUS Disconnect / CoA recovery using the learned per-session AP/NAS source IP
- AAA-initiated STa ASR/STR cleanup path
- Duplicate Create Session / StarOS context-replacement protection
- Post-activation datapath validation
- Graceful shutdown improvements
Items still being hardened:
- GTP-U Echo support through Linux kernel GTP generic netlink where supported, with safe fallback to the kernel-registered GTP-U socket
- Broader AP compatibility testing
- Long-duration stability testing
This TWAG can use the VectorCore AAA Server as the 3GPP AAA component for non-3GPP access authentication and authorization.
The AAA server provides the authentication path used by:
- TWAG access via STa toward the 3GPP AAA Server
- AAA/HSS interaction via SWx
- AAA/PGW interaction via S6b
UE
|
| WPA2/WPA3 Enterprise / 802.1X / EAP-AKA′
|
Wi-Fi AP / NAS
|
| RADIUS Access-Request / Access-Challenge / Access-Accept
| RADIUS Accounting-Start / Interim / Stop
|
VectorCore TWAG
|
| Diameter STa / EAP exchange
|
3GPP AAA / DRA / HSS
|
VectorCore TWAG
|
| GTP-C Create Session / Delete Session
| GTP-U user-plane through Linux kernel GTP
|
PGW / P-GW
|
Internet / IMS / MMS APNs
The TWAG has two main sides:
Access side:
WLAN-facing Ethernet/VLAN interface
DHCP
ARP proxy
MAC/IP authorization binding
optional RADIUS VLAN assignment
Core side:
Diameter STa toward AAA/DRA
GTP-C toward PGW
GTP-U through Linux kernel GTP
The AP acts as the RADIUS client/NAS. The TWAG acts as the RADIUS server.
The UE performs EAP-AKA′ through the AP. The AP wraps EAP payloads in RADIUS Access-Request messages. The TWAG forwards the EAP exchange to the AAA/HSS path over Diameter STa.
On EAP success, TWAG sends RADIUS Access-Accept with:
- EAP success
- MS-MPPE-Send-Key
- MS-MPPE-Recv-Key
- Optional VLAN assignment
- Session-Timeout / Termination-Action when configured
Access-Accept is only sent after PGW Create Session succeeds and local GTP, routing, DHCP/ARP, and access bindings are ready.
TWAG also keeps a RADIUS authentication cache keyed primarily by MAC + IMSI + APN. This cache is separate from the active PGW/GTP data session. Accounting-Stop clears the active data session by default, but the authentication cache remains valid until the RADIUS Session-Timeout expires unless radius.accounting.clear_auth_cache_on_stop is enabled.
This matters for AP/UE behavior that reuses cached 802.1X authorization or PMK state after Wi-Fi disable/enable. In that case the AP may send Accounting-Start without a fresh Access-Request/EAP exchange. TWAG treats Accounting-Start as an AP-side session-start signal, and if valid authentication cache exists, it rebuilds PGW/GTP/routing/access state before DHCP is allowed.
TWAG connects to the configured AAA/DRA peer over Diameter and exchanges EAP payloads using STa.
The AAA/HSS side is expected to return EAP-AKA′ challenge/success and EAP keying material.
After successful authentication, TWAG creates a PGW session.
TWAG sends:
GTP-C Create Session Request
PGW returns:
GTP-C Create Session Response
subscriber IP
PGW control TEID
PGW user-plane F-TEID
TWAG later sends:
GTP-C Delete Session Request
when the session is detached, recovered, replaced, or cleaned up.
GTP-U is handled through Linux kernel GTP. TWAG programs PDP/session state into the kernel and uses a gtp0 interface for user-plane forwarding.
TWAG must not create a competing userspace UDP/2152 socket that fights the kernel GTP datapath.
For GTP-U Echo, TWAG first tries the kernel GTP generic netlink GTP_CMD_ECHOREQ path. Some kernels expose the command but reject it for GTP devices created with userspace-supplied UDP file descriptors. In that case TWAG sends Echo Requests using the same UDP/2152 socket already registered with the kernel GTP device. This is not a second listener and does not take G-PDUs away from the kernel datapath.
The PGW assigns the subscriber IP, but the Wi-Fi UE expects normal DHCP and ARP behavior.
TWAG provides:
DHCP Offer/Ack with the PGW-assigned subscriber IP
ARP proxy replies for a configurable virtual gateway IP
The virtual gateway IP is a TWAG-side construct used for WLAN access behavior. It must not conflict with the PGW subscriber pool.
twag:
name: twag01
realm: epc.mnc435.mcc311.3gppnetwork.org
logging:
level: info
file: /tmp/vectorcore-twag/twag.log
access:
interface: enp0s8
gateway_ip: 100.64.0.1
netmask: 255.255.255.0
dns:
- 8.8.8.8
- 1.1.1.1
dhcp:
enabled: true
recover_from_auth_cache: true
lease_time_seconds: 600
renewal_time_seconds: 300
rebinding_time_seconds: 525
arp_proxy:
enabled: true
forwarding:
enabled: true
radius:
enabled: true
listen_addr: 0.0.0.0:1812
secret: testing123
vlan_id: 10
allowed_source_subnets:
- 192.168.105.0/24
auth_cache:
enabled: true
default_ttl_seconds: 3600
max_ttl_seconds: 86400
access_accept:
session_timeout_seconds: 3600
termination_action: radius_request
idle_timeout_seconds: 0
accounting:
enabled: true
listen_addr: 0.0.0.0:1813
secret: testing123
clear_session_on_stop: true
clear_auth_cache_on_stop: false
interim_update_liveness: true
accounting_off_action: mark_at_risk
start_without_session_action: recover_if_auth_valid
start_without_auth_action: disconnect
dynamic_authorization:
default_port: 3799
secret: testing123
prefer_discovered_nas: true
aaa:
sta:
origin_host: twag01.epc.mnc435.mcc311.3gppnetwork.org
origin_realm: epc.mnc435.mcc311.3gppnetwork.org
destination_realm: epc.mnc435.mcc311.3gppnetwork.org
destination_host: dra01.epc.mnc435.mcc311.3gppnetwork.org
peer_addr: 10.90.250.35:3868
subscriber:
default_apn: internet
default_realm: ims.mnc435.mcc311.3gppnetwork.org
gtp:
local_gtpc_ip: 10.90.250.186
local_gtpu_ip: 10.90.250.186
remote_pgw_gtpc_ip: 10.90.250.92
remote_pgw_gtpu_ip: 10.90.250.92
apn: internet
charging_characteristics: "0800"
kernel_interface: gtp0
control_echo:
enabled: true
interval_seconds: 60
timeout_seconds: 5
max_failures: 3
startup_probe: true
user_echo:
enabled: true
mode: kernel_netlink
interval_seconds: 60
timeout_seconds: 5
max_failures: 3
startup_probe: true
require_kernel_support: false
session_recovery:
enabled: true
reason_gtpu_error_indication: true
recovery_window_seconds: 60
stale_client_grace_seconds: 10
cleanup_on_duplicate_attach: true
allow_same_mac_reattach: true
reject_old_dhcp_ip: true
dhcp_stale_request_action: nak
accounting_start_recovery:
enabled: true
cooldown_seconds: 10
max_attempts_per_minute: 3
radius_disconnect:
enabled: true
nas_port: 3799
secret: testing123
timeout_seconds: 3
retries: 2
request_type: disconnect
wait_for_accounting_stop: true
accounting_stop_timeout_seconds: 10
fallback_to_recovery_tombstone: true
session_lifecycle:
duplicate_attach_policy: reuse_existing
duplicate_attach_cleanup_timeout_seconds: 5
suppress_duplicate_create_session: true
per_subscriber_lock_timeout_seconds: 10
post_activation_validation:
enabled: true
fail_action: trigger_recovery
first_packet_timeout_seconds: 0
routing:
enable_ip_forwarding: true
disable_rp_filter: true
policy_routing: true
policy_table: 200
policy_priority: 10000The access.gateway_ip value is the virtual gateway that TWAG presents to the UE through DHCP and ARP proxy.
This address must be reserved or excluded from the PGW subscriber pool.
Unsafe:
PGW pool: 100.64.0.0/24
TWAG gateway_ip: 100.64.0.1
PGW can assign 100.64.0.1 to a UE
Safer:
TWAG virtual gateway: 100.64.0.1
PGW allocatable pool: 100.64.0.2 - 100.64.0.254
If the AP expects dynamic VLAN assignment, TWAG can return VLAN attributes in Access-Accept.
Typical attributes:
Tunnel-Type = VLAN
Tunnel-Medium-Type = IEEE-802
Tunnel-Private-Group-ID = <vlan_id>
The configured AP switchport must carry that VLAN correctly.
For 802.1X, the AP/NAS decides how long the client remains authorized. TWAG can influence this using RADIUS Access-Accept attributes:
Session-Timeout
Termination-Action = RADIUS-Request
TWAG can listen for RADIUS Accounting on UDP/1813.
Important Accounting status types:
Accounting-Start
Accounting-Interim-Update
Accounting-Stop
Accounting-On
Accounting-Off
Accounting-Start is used to bind AP/NAS accounting state to the active TWAG session. Accounting-Stop is treated as authoritative evidence that the AP-side client session ended, so TWAG clears the active PGW/GTP/routing/access/DHCP/ARP state for the matched session.
Accounting-Stop does not necessarily mean the UE or AP discarded cached 802.1X/PMK authorization. By default, TWAG keeps the RADIUS authentication cache after Accounting-Stop:
radius:
accounting:
clear_session_on_stop: true
clear_auth_cache_on_stop: falseThis lets a later Accounting-Start rebuild the data session if the cached authorization is still within Session-Timeout.
The authentication cache records successful RADIUS/EAP authorization separately from active PGW/GTP session state.
Primary cache key:
MAC + IMSI + APN
The cache expires based on the RADIUS Session-Timeout returned in Access-Accept. If no explicit value is available, TWAG uses:
radius:
auth_cache:
default_ttl_seconds: 3600
max_ttl_seconds: 86400DHCP does not use this cache as direct authorization. DHCP still requires an active PGW/GTP session with a subscriber IP. The cache only allows TWAG to trigger a recovery attach and recreate the active session.
TWAG can use RADIUS Dynamic Authorization to force AP-side reauthentication when TWAG has lost the PGW/GTP session.
This normally uses UDP/3799.
TWAG does not use a static NAS IP for Disconnect/CoA. Each AP/NAS may have a DHCP-assigned address, so TWAG stores the per-session source from the RADIUS Access-Request. If the request contains NAS-IP-Address, that value is stored; otherwise TWAG stores the UDP source IP of the Access-Request. Recovery Disconnect/CoA is sent back to that learned session source on radius_disconnect.nas_port.
The AP/controller must support and allow:
RADIUS Disconnect-Request
RADIUS CoA-Request
shared secret
authorized dynamic authorization client IP
If the AP does not support this, TWAG falls back to recovery tombstone behavior and waits for natural reauth.
1. UE associates to AP.
2. AP sends RADIUS Access-Request to TWAG.
3. TWAG exchanges EAP-AKA′ with AAA over Diameter STa.
4. AAA returns EAP success and MSK.
5. TWAG defers RADIUS Access-Accept until the PGW session is usable.
6. TWAG creates PGW session over GTP-C.
7. PGW assigns subscriber IP.
8. TWAG installs kernel GTP PDP/session.
9. TWAG installs routing and access binding for MAC/IP.
10. TWAG prepares DHCP/ARP authorization.
11. TWAG returns RADIUS Access-Accept.
12. UE sends DHCP.
13. TWAG replies with PGW-assigned IP.
14. TWAG answers ARP for virtual gateway.
15. UE traffic flows through GTP-U.
1. Session is detached or shutdown begins.
2. TWAG sends GTP-C Delete Session.
3. TWAG removes kernel GTP PDP/session.
4. TWAG removes routes/policy rules.
5. TWAG removes access binding.
6. TWAG releases DHCP/access state.
7. Session is marked terminated.
Some AP/UE combinations reuse cached 802.1X authorization after Wi-Fi is disabled and re-enabled. In that case the AP may send a new Accounting-Start without a fresh Access-Request/EAP exchange.
TWAG handles that case as:
1. UE disconnects or disables Wi-Fi.
2. AP sends Accounting-Stop.
3. TWAG clears active PGW/GTP/routing/access/DHCP/ARP state.
4. TWAG keeps the RADIUS auth cache until Session-Timeout expires.
5. UE reconnects using cached 802.1X/PMK authorization.
6. AP sends Accounting-Start without fresh EAP.
7. TWAG finds valid auth cache for MAC + IMSI + APN.
8. TWAG creates a new PGW session.
9. TWAG installs kernel GTP, routing, and access bindings.
10. TWAG binds the new Acct-Session-Id to the recovered TWAG session.
11. DHCP is allowed only after the recovered session is active.
Expected recovery logs:
Accounting-Start without active TWAG session; valid auth cache found, triggering recovery attach
Accounting session bound to recovered TWAG session
Accounting-Start recovery completed
If no valid auth cache exists, TWAG does not create PGW/GTP state and can send RADIUS Disconnect/CoA depending on:
radius:
accounting:
start_without_auth_action: disconnectTWAG performs recovery when it sees a mapped GTP-U Error Indication, stale TEID, or similar session failure.
1. TWAG marks the session failed/recovering.
2. TWAG creates a recovery tombstone.
3. TWAG cleans local PGW/kernel/access/DHCP/ARP state.
4. TWAG sends RADIUS Disconnect/CoA if configured.
5. If the AP reauthenticates the UE, TWAG creates a new clean session.
6. If the AP does not respond, TWAG keeps the tombstone and safely rejects stale DHCP/ARP.
StarOS PGW treats duplicate Create Session for the same IMSI/APN/default bearer as context replacement.
The PGW may log:
EBI Collision detection logic resulted in CONTEXT_REPLACEMENT
Create Session Request ... in ACTIVE state
This is context replacement
TWAG must prevent this during normal operation.
Rules:
One active or activating PGW session per IMSI/APN/default bearer.
Local access correlation also tracks MAC/IMSI/APN.
If duplicate EAP success occurs for an already-active session, reuse the existing session.
If replacement is required, delete and clean the old session before sending a new Create Session.
Do not rely on PGW context replacement as normal behavior.
TWAG sends periodic GTP-C Echo Requests and handles PGW-initiated GTP-C Echo Requests. TWAG enforces a minimum outbound GTP-C/GTP-U Echo interval of 60 seconds; shorter configured intervals are clamped and logged as warnings.
Expected logs:
GTP-C echo response received
GTP-C echo request received
GTP-C echo response sent
GTP-U uses the Linux kernel GTP datapath.
Modern Linux kernels may support GTP-U Echo through the kernel GTP generic netlink API. TWAG configures this with gtp.user_echo.mode: kernel_netlink and never opens a second competing UDP/2152 socket for echo.
Runtime behavior:
try GTP_CMD_ECHOREQ through kernel generic netlink
if the kernel accepts it, the packet is sent by the kernel GTP path
if the kernel rejects it as unsupported for this GTP device, use the UDP/2152 socket already registered with gtp.ko
observe Echo Response on the existing GTP-U control read path
do not consume G-PDUs in userspace
The fallback runtime log uses:
mode="kernel_registered_socket"
That means TWAG is using the same socket/Fd registered with the Linux GTP device, not a new UDP listener.
TWAG should:
detect kernel GTP-U Echo support
trigger GTP-U Echo Request through kernel netlink when supported by the active device
fall back to the existing kernel-registered UDP/2152 socket when netlink echo is unavailable
observe Echo Response through the existing GTP-U control path
avoid creating a competing UDP/2152 userspace socket
If all supported GTP-U Echo paths are unavailable and require_kernel_support is false, TWAG disables user_echo and continues to rely on GTP-U Error Indication handling. If require_kernel_support is true, startup fails when GTP-U Echo cannot be enabled.
Useful checks:
uname -r
modinfo gtp | head -50
grep -R "GTP_CMD_ECHOREQ" /usr/include/linux/gtp.h /usr/src/linux-headers-$(uname -r)/include/uapi/linux/gtp.h 2>/dev/nullTypical Go project flow:
go mod tidy
go build -o bin/twag ./cmd/twagIf a Makefile exists:
make buildExample:
sudo ./bin/twag -c /root/twag.yamlor:
sudo ./bin/twag -d -c /root/twag.yamlTWAG requires sufficient privileges for:
raw socket access
network interface access
Linux kernel GTP netlink operations
routing table updates
policy routing
IP forwarding / rp_filter changes
UDP 1812 RADIUS listen
UDP 1813 RADIUS Accounting listen
GTP-C/GTP-U sockets
In practice, run as root during lab testing.
Required or expected:
Linux kernel with gtp.ko
CAP_NET_ADMIN
CAP_NET_RAW
iproute2
tcpdump / tshark for troubleshooting
Load/check GTP module:
modprobe gtp
lsmod | grep gtpCheck interface:
ip link show gtp0Check routing:
ip rule show
ip route show table 200
ip route get 8.8.8.8 from <subscriber-ip>tcpdump -ni any -s 0 -w /tmp/twag-radius.pcap 'udp port 1812 or udp port 1813'Expected sequence:
Access-Request
Access-Challenge
Access-Request
Access-Challenge
Access-Request
Access-Accept
Accounting-Start
tcpdump -ni any -s 0 -w /tmp/twag-gtpc.pcap 'udp port 2123'Look for:
Create Session Request
Create Session Response
Delete Session Request
Delete Session Response
Echo Request
Echo Response
tcpdump -ni any -s 0 -w /tmp/twag-gtpu.pcap 'udp port 2152'Expected Echo packets when gtp.user_echo.enabled: true:
TWAG -> PGW GTP-U Echo Request, TEID 0
PGW -> TWAG GTP-U Echo Response, TEID 0
Useful tshark:
tshark -r /tmp/twag-gtpu.pcap -Y "gtp" \
-T fields \
-e frame.time_relative \
-e ip.src \
-e ip.dst \
-e udp.srcport \
-e udp.dstport \
-e gtp.message \
-e gtp.teid \
-e gtp.seqFilter Echo/Error only:
tshark -r /tmp/twag-gtpu.pcap \
-Y "gtp.message == 1 || gtp.message == 2 || gtp.message == 26" \
-T fields \
-e frame.time_relative \
-e ip.src \
-e ip.dst \
-e udp.srcport \
-e udp.dstport \
-e gtp.message \
-e gtp.teidMessage types:
0x01 = Echo Request
0x02 = Echo Response
0x1a = Error Indication
0xff = G-PDU / T-PDU
tcpdump -ni <access-interface> -e -vvv 'arp or udp port 67 or udp port 68'Expected:
DHCP Discover
DHCP Offer
DHCP Request
DHCP Ack
ARP who-has gateway_ip
ARP reply from TWAG
If logs show:
DHCP ignored unauthorized client
authorized=false
possible causes:
UE/AP still thinks 802.1X is authorized but TWAG removed PGW session.
Recovery tombstone exists.
RADIUS auth cache expired or was cleared.
Accounting-Start was not received or was dropped by allowed_source_subnets.
RADIUS Disconnect/CoA failed, is not enabled, or the AP does not allow TWAG as a dynamic authorization client.
UE has not re-run EAP yet.
Recommended fix path:
Enable RADIUS Disconnect/CoA if AP supports it.
Allow TWAG's RADIUS source IP as a dynamic authorization client on the AP/controller.
Set Session-Timeout and Termination-Action in Access-Accept.
Enable RADIUS Accounting on the AP/controller and point it at TWAG UDP/1813.
Keep radius.accounting.clear_auth_cache_on_stop=false when AP/UE cached PMK reconnect is expected.
Ensure recovery tombstone logs distinguish stale recovery clients from truly unauthorized clients.
If PGW sends:
GTP-U Error Indication
it means PGW rejected the TEID used by TWAG.
Common causes:
duplicate Create Session caused PGW context replacement
stale local kernel GTP PDP
PGW deleted bearer/session
wrong F-TEID selected
session replacement race
Check StarOS logs for:
CONTEXT_REPLACEMENT
EBI Collision
Create Session Request in ACTIVE state
Session deleted
Verify:
ip route get 8.8.8.8 from <subscriber-ip>
ip rule show
ip route show table 200
ip neigh show dev <access-interface>
ip link show gtp0Capture simultaneously:
tcpdump -ni <access-interface> -w /tmp/access.pcap 'host <subscriber-ip> or arp or udp port 67 or udp port 68'
tcpdump -ni any -w /tmp/gtpu.pcap 'udp port 2152'Do not create a second userspace UDP/2152 listener competing with Linux kernel GTP.
Use kernel GTP netlink support where available. If the running kernel rejects GTP_CMD_ECHOREQ for the active GTP device, use only the UDP/2152 socket already registered with the kernel GTP device.
Cleanup can be triggered by:
GTP-U Error Indication
duplicate attach
shutdown
session recovery
manual detach
PGW context not found
Removing an already-removed route, neighbor, PDP context, or session should not panic.
Ctrl+C should:
cancel root context
stop RADIUS
stop accessside loops
detach active sessions
stop GTP echo loops
close GTP sockets/fds
stop Diameter
exit cleanly
Every goroutine that reads sockets or raw frames should have:
context cancellation
fd/socket close
WaitGroup tracking
stop timeout
Successful attach:
RADIUS Access-Request received
STa EAP answer received state=success
RADIUS Access-Accept deferred until PGW session ready
authorized subscriber attach requested
GTP-C Create Session Request sent
GTP-C Create Session Response received gtp_cause=16
PGW assigned subscriber IP
kernel GTP session added
access forwarding neighbor installed
session active
PGW session ready; sending RADIUS Access-Accept
RADIUS Access-Accept sent
RADIUS Accounting-Start received
DHCP Ack sent
ARP proxy reply sent
Healthy operation:
GTP-C echo response received consecutive_failures=0
GTP-C echo request received
GTP-C echo response sent
Diameter watchdog success
DHCP Ack sent
ARP proxy reply sent
Recovery:
GTP-U Error Indication received mapped=true
session recovery pending
RADIUS Disconnect-Request sent for session recovery
fresh RADIUS/EAP reauth observed
session recovery completed
Cached 802.1X / PMK reconnect:
RADIUS Accounting-Stop received; clearing TWAG session
RADIUS Accounting-Stop processed; active session cleared but auth cache retained
RADIUS Accounting-Start received session_id=""
Accounting-Start without active TWAG session; valid auth cache found, triggering recovery attach
Accounting session bound to recovered TWAG session
Accounting-Start recovery completed
DHCP Ack sent
Duplicate attach protection:
PGW Create Session decision decision=reuse_existing
duplicate attach suppressed; existing active PGW session reused
- Only authorized MAC/IMSI/APN sessions should receive DHCP/ARP service.
- Do not ACK DHCP for a client without an active PGW/GTP session.
- RADIUS shared secrets must be protected.
- AP/controllers should only accept RADIUS Dynamic Authorization from TWAG's trusted RADIUS source IP.
- Avoid exposing the TWAG management/logging interface on untrusted networks.
- The access VLAN should be isolated from general LAN traffic.
VectorCore TWAG
