Portal Integrity Checks
Portal integrity checks let Nzyme continuously experience a network the way a freshly connected guest device would. A tap joins the network with its own MAC address, requests a DHCP lease, calls the same connectivity check URLs that operating systems use to detect captive portals, follows every redirect, and records what it is presented with. Each check is stored as a report in Nzyme, where you can review the DHCP lease, every redirect hop, the TLS certificates and the full content of the portal page that a guest would have seen.
This makes it possible to detect tampered captive portals, hijacked DNS, rogue gateways and injected content on guest networks, which are attack techniques that are invisible to classic WiFi and network monitoring because the traffic itself looks perfectly normal.
Portal integrity checks are part of the Ethernet subsystem and are configured on nzyme-tap. You find the recorded
reports in the web interface under Ethernet -> Portal Integrity.
Why Portal Integrity Checks?
CaptiveCrunch
In July 2026, Microsoft Threat Intelligence published details about a campaign it named CaptiveCrunch. The actor, tracked as Storm-2945 and considered a sub-cluster of Midnight Blizzard (attributed to the Russian SVR), had been manipulating DNS and HTTP traffic on captive portal networks of hotels, conference centers and other hospitality venues since May 2026. Activity resumed in late September 2026.
Travelers who connected to the WiFi of an affected venue were not simply shown the usual sign-in page. Instead, their traffic was routed through actor-controlled infrastructure and they were presented with:
- Lookalike Microsoft sign-in pages that harvested credentials
- Device code phishing prompts that abused the legitimate OAuth device code flow to take over accounts
- Fake Windows, browser or operating system update screens (a so-called ClickFix technique) that instructed the victim to run PowerShell commands or install APK files, delivering the CornFlake remote access trojan and the ChocoShell infostealer
Microsoft noted commonalities in the equipment and management systems used across affected networks, which points at compromised shared captive portal services and managed service providers rather than individually hacked hotels. In other words: the attacker did not need to be on site. One upstream compromise was enough to tamper with the portals of many venues at once, and the venues themselves had no easy way to notice.
The guests had no way to notice either. The network was real, the SSID was real, the connection was encrypted, and the portal looked like a portal. There was no rogue access point to detect and no deauthentication storm to alert on. The only thing that had changed was what the portal did.
Other Scenarios
CaptiveCrunch is the most prominent example, but the same class of problem shows up in many forms:
- Compromised portal appliance or gateway. A vulnerable or misconfigured captive portal controller is taken over and starts serving modified pages or redirecting to external hosts.
- Compromised upstream provider. A hospitality or venue WiFi provider that manages hundreds of sites is breached, and all of its sites are affected simultaneously.
- Rogue DHCP server. A device on the guest segment hands out leases with an attacker-controlled gateway or DNS server. Clients are now routed through, or resolved by, the attacker.
- DNS hijacking. The legitimate gateway stays in place, but its upstream resolver configuration is changed so that connectivity check hostnames and sign-in domains resolve to attacker infrastructure.
- Legitimate drift. A vendor updates the portal, a certificate expires, the gateway is replaced. These are not attacks, but they are changes that you want to know about and that often go unnoticed for months.
Nzyme's WiFi features observe networks from the outside, looking at frames in the air. Portal integrity checks add the inside view: they observe what a guest device actually receives after it joins. Both views together cover scenarios that each one alone would miss.
How Captive Portals Work
A captive portal is a mechanism that forces a newly connected device to visit a specific web page before it is allowed to use the network. Understanding the sequence is important, because every single step of it is controlled by the network and can be manipulated.
- Association and DHCP. The device joins the network and requests an IP configuration via DHCP. The DHCP response contains the IP address, the default gateway and the DNS servers the device should use.
- Connectivity check. The operating system immediately requests a well-known URL over plain HTTP, for example
http://detectportal.firefox.com/. It expects a specific response, such as the textsuccessor an HTTP status204. - Interception. The captive portal gateway intercepts this request. Depending on the implementation it does one
of the following:
- Answers the DNS query for the check hostname with its own IP address
- Lets DNS resolve correctly but intercepts the TCP connection to port 80 and answers itself
- Returns an HTTP redirect (status
302or307) with aLocationheader pointing at the portal page - Returns an HTML page that contains a
<meta http-equiv="refresh">tag or a JavaScriptlocation.hrefassignment that sends the browser to the portal page
- Portal page. The redirect chain ends at the portal page, which is usually served over HTTPS and asks the user to accept terms, enter a room number or voucher code, or sign in with an account.
- Authorization. Once the user completes the portal, the gateway adds the client's MAC and IP address to an allow list. The next connectivity check passes through to the real destination, the operating system sees the expected response and the device is online.
Because the device fully trusts the DHCP server, the DNS servers and the redirects it receives, an attacker controlling any one of them controls what the user sees. The connectivity check URLs are a particularly attractive target because every device requests them automatically, without the user doing anything.
Connectivity Check URLs
The following URLs are commonly used by operating systems and browsers. The expected response is what a device receives on a network without a captive portal, or after the portal has been passed.
| Operating System / Vendor | URL | Expected Clean Response |
|---|---|---|
| Firefox | http://detectportal.firefox.com/ |
Status 200, body success |
| Firefox | http://detectportal.firefox.com/canonical.html |
Status 200, small HTML page |
| Apple (macOS, iOS) | http://captive.apple.com/hotspot-detect.html |
Status 200, HTML page with body Success |
| Android, ChromeOS | http://connectivitycheck.gstatic.com/generate_204 |
Status 204, empty body |
| Android | http://www.google.com/generate_204 |
Status 204, empty body |
| Windows | http://www.msftconnecttest.com/connecttest.txt |
Status 200, body Microsoft Connect Test |
| Windows (legacy) | http://www.msftncsi.com/ncsi.txt |
Status 200, body Microsoft NCSI |
| Ubuntu (NetworkManager) | http://connectivity-check.ubuntu.com/ |
Status 204, empty body |
| GNOME | http://nmcheck.gnome.org/check_network_status.txt |
Status 200, body NetworkManager is online |
| KDE | http://networkcheck.kde.org/ |
Status 200, body OK |
These are plain HTTP URLs on purpose. A captive portal can only redirect a request it can read, and it cannot read HTTPS traffic without triggering certificate warnings. This is also why connectivity checks over HTTP are the exact place where CaptiveCrunch inserted itself.
How Nzyme Monitors Captive Portals
A portal integrity check is a probe that runs on nzyme-tap. Each probe is tied to one network interface and
repeats the following cycle at the configured interval:
- Build an independent network stack. The probe opens the interface in promiscuous mode and runs its own Ethernet, IP, TCP, DHCP and DNS implementation on top of it, using the MAC address from the probe configuration. The operating system of the tap host is not involved in any of this traffic. This is what allows the probe to act like a completely separate guest device.
- Request a DHCP lease. The probe sends a DHCP request and records the assigned address, gateway, DHCP server and DNS servers. Each report carries this lease information, so you can see when the network starts handing out different gateways or resolvers.
- Resolve and request each control URL. For each configured control URL, the probe resolves the hostname using
the DNS servers from the lease, connects, and sends a plain
GETrequest. - Follow the redirect chain. If the response is a redirect, the probe follows it. It understands
Locationheaders,Refreshheaders,<meta http-equiv="refresh">tags and simple JavaScript redirects (location.href,location.replace,window.location). It stops after the configured maximum number of redirects or when a response is not a redirect. - Record every hop. For each hop, the probe records the URL, the IP address the hostname resolved to, the HTTP status, the URL it was redirected to, whether the response was read completely, and the full raw response including headers and body. For HTTPS hops it additionally records the presented certificate chain, the negotiated TLS version and the cipher suite. The probe does not abort on invalid certificates, because the point is to record exactly what is presented.
- Submit the report. Each control URL results in one report that is sent to
nzyme-nodeand stored under the Ethernet subsystem of the tenant the tap belongs to. Nzyme enriches the resolved IP addresses of each hop with GeoIP and ASN information where available.
The probe never interacts with the portal. It does not accept terms, submit forms or sign in. This is intentional: because the probe MAC address never gets authorized, the gateway treats it as a new guest in every cycle and presents the full portal flow every time. You get a continuous record of what a guest sees, not just a one-time snapshot.
Info
The probe does not currently alert on its own. It records reports that you can search, filter and compare in the web interface or via the REST API. See What to Look For below for the indicators that are worth watching.
Deploying a Portal Integrity Probe
Where to Place the Probe
A probe has to see the network exactly like a guest client does. This is a different placement than the typical Ethernet tap on a mirror port, which observes traffic but does not participate in the network.
- WiFi guest networks. The most common and most important case. Use a small, dedicated machine such as a Raspberry Pi with a WiFi client interface that joins the guest SSID as a normal station. Place it where guests would connect, for example in a lobby, conference area or meeting floor. A separate probe per SSID is recommended, because different SSIDs often map to different VLANs, gateways and portals.
- Wired guest networks. Connect a dedicated interface of the tap to a guest-facing switch port or guest VLAN.
- Multiple sites. Run one probe per site. The probe name you choose in the configuration is stored with every report, so you can filter and compare sites in the web interface.
A tap that runs a portal integrity probe can also run normal WiFi monitoring and Ethernet capture at the same time, as
long as each function uses its own interface. Do not use the same interface for both a probe and an
ethernet_interfaces or wifi_interfaces configuration.
The Probe Needs a Dedicated Interface
The probe interface must not be used by the host operating system at all. No IP address, no DHCP client, no routes.
The reason is that the probe runs its own network stack on the interface. If the host kernel also has an IP configuration there, both stacks see the same frames and interfere with each other: the kernel would run its own DHCP client and acquire a second lease, respond to ARP requests, and send TCP resets for connections it does not know about, which breaks the probe's connections. On WiFi interfaces, where the probe has to share the hardware MAC address with the kernel, this interference is guaranteed.
The requirement differs slightly between wired and wireless interfaces:
- Ethernet: The interface only needs to have link. Nothing else is required from the host.
- WiFi: The host still has to associate the interface with the access point and perform the WPA handshake, because this happens below the level at which the probe operates. The host must do this without configuring IP on the interface.
Also make sure that the probe interface is not listed in the ethernet_interfaces section of your nzyme-tap
configuration and that no other software (such as a DHCP client started from a system script) touches it.
Configuring the Interface with NetworkManager
Raspberry Pi OS and many desktop distributions use NetworkManager. The cleanest way to hand an interface to the probe is to create a connection profile that brings the link up but disables all IP configuration.
For an Ethernet interface called eth1:
nmcli connection add type ethernet ifname eth1 con-name nzyme-probe \
ipv4.method disabled ipv6.method disabled connection.autoconnect yes
nmcli connection up nzyme-probe
For a WiFi interface called wlan1 joining a WPA2 personal network:
nmcli connection add type wifi ifname wlan1 con-name nzyme-probe \
ssid "Guest WiFi" \
wifi-sec.key-mgmt wpa-psk wifi-sec.psk "the-guest-password" \
wifi.cloned-mac-address permanent \
ipv4.method disabled ipv6.method disabled connection.autoconnect yes
nmcli connection up nzyme-probe
For an open network, leave out the two wifi-sec.* arguments. The wifi.cloned-mac-address permanent setting
disables NetworkManager's MAC address randomization for this connection, so the interface keeps its hardware address.
Verify that the interface is up and has no IPv4 address:
You should see state UP but no inet line. An IPv6 link-local address (inet6 fe80::...) is harmless.
Tip
An alternative for Ethernet interfaces is to tell NetworkManager to ignore the interface entirely by adding an
unmanaged-devices=interface-name:eth1 entry to the [keyfile] section of a file in
/etc/NetworkManager/conf.d/. Note that an unmanaged interface is not brought up automatically, so you need to
run ip link set eth1 up at boot, for example from a systemd unit. The connection profile approach above avoids
this.
Configuring the Interface with Netplan
Ubuntu Server uses Netplan. Add the probe interface with DHCP disabled and no addresses. For an Ethernet interface:
For a WiFi interface:
network:
version: 2
wifis:
wlan1:
dhcp4: false
dhcp6: false
link-local: []
optional: true
access-points:
"Guest WiFi":
password: "the-guest-password"
Apply with netplan apply and verify with ip addr show as described above.
Choosing the MAC Address
The mac configuration is required. What you set it to depends on the interface type:
- Ethernet: Choose any locally administered unicast MAC address that is not used elsewhere on the network. Locally
administered addresses have the second hex digit set to
2,6,AorE, for exampleAE:3F:99:DF:3D:F5. Use a different address for each probe. - WiFi: Set it to the hardware address of the WiFi interface. Access points only accept frames from the station address that completed the association, so a different address will not work on wireless interfaces.
Keep in mind that the probe MAC address will show up in the DHCP logs, client lists and portal statistics of the network you are monitoring. If you do not own the network, inform the operator.
Choosing Control URLs
Control URLs are the URLs the probe requests in every cycle. Choose them based on what you want to observe:
- At least one real connectivity check URL from the table above, for example
http://detectportal.firefox.com/. Captive portals are built to intercept exactly these URLs, and so are attacks against captive portals. This is the most important control URL. - Connectivity check URLs of other platforms, if your guests are a mixed population. A targeted attack could tamper with the Android check but leave the Firefox check alone.
- An HTTPS URL, for example the sign-in domain your organization uses or a well-known public site. Portals cannot redirect HTTPS requests without presenting a certificate, so this hop shows you what certificate is being presented to guests and whether traffic is being intercepted and re-encrypted.
- A URL you control. If you host a page with a known body, any deviation of the recorded content is a clear signal.
A few constraints apply:
- Only
http://andhttps://URLs are supported. - Only IPv4 is supported. Hostnames are resolved to
Arecords. Literal IPv6 addresses in control URLs are not supported. - Responses are read up to 8 MiB. Larger responses are stored truncated and marked accordingly.
- The probe uses only the first DNS server from the DHCP lease for resolution.
Configuration
Add one [portal_integrity_checks.<name>] section to your nzyme-tap configuration file for each probe. The <name>
is the probe name that will be stored with every report. You can configure as many probes as you have dedicated
interfaces.
[portal_integrity_checks.lobby_guest_wifi]
active = true
interface = "wlan1"
mac = "F4:C8:8A:2E:DF:74"
interval_minutes = 5
control_urls = ["http://detectportal.firefox.com/", "http://captive.apple.com/hotspot-detect.html"]
max_redirects = 10
| Variable | Description |
|---|---|
active |
Set to true to start this probe when nzyme-tap starts. Inactive probes are ignored. |
interface |
Name of the dedicated network interface to run the probe on. See above. |
mac |
The MAC address the probe uses as its own. See above. |
interval_minutes |
How often a check cycle runs, in minutes. Must be at least 1. Every cycle requests a new DHCP lease and produces one report per control URL, so very short intervals generate a lot of data on busy deployments. |
control_urls |
List of URLs to request in every cycle. At least one URL is required. |
max_redirects |
How many redirects to follow for each control URL before the probe stops and records the chain as is. |
All changes to the configuration file require a restart of nzyme-tap to take effect.
After the restart, the tap log shows a line like Initializing portal integrity check [lobby_guest_wifi]. The first
reports appear in the web interface shortly after the first cycle completes. If no reports show up, start nzyme-tap
with the log level set to debug to see the DHCP, DNS and HTTP steps of each cycle.
Required Subsystem and Permissions
Portal integrity checks belong to the Ethernet subsystem. The subsystem must
be enabled for the organization and tenant that the tap belongs to, otherwise nzyme-node rejects the reports.
All users with access to the Ethernet subsystem of a tenant can view portal integrity reports.
Reading Reports
Open Ethernet -> Portal Integrity in the web interface. The page respects the global tap selector and time range selector, like all other Ethernet pages.
Report List
Every row is one control URL request from one cycle of one probe. The list shows the probe name, the control URL that was requested, the Final URL (the URL of the last hop, which is the page a guest would have ended up on), the hop count and the time of the probe.
You can filter the list by probe name, probe interface, probe MAC address, DHCP server address, assigned IP address, assigned gateway address, assigned DNS server address, control URL, final URL and hop count. Filters are useful to isolate one probe or to search for unexpected values, for example all reports where the gateway address is not the one you expect.
Report Details
Click on a report ID to open the details page. It has four sections:
- Details: Probe name, control URL, hop count and probe time.
- Network: The interface and MAC address the probe used, and the DHCP lease it received: IP address, gateway address, DHCP server address and DNS servers.
- Presented Portal: The URL of the last hop and the complete raw HTTP response, including headers and body. This is what a guest browser would have rendered.
- Hops: A table of every hop in the redirect chain with the following columns:
| Column | Description |
|---|---|
| # | Position in the chain, starting at 0 for the control URL itself. |
| URL | The URL that was requested in this hop. |
| Status | The HTTP status code of the response. |
| Address | The IP address the hostname resolved to and that the probe connected to. Links to the asset if Nzyme knows it. |
| Followed To | The redirect target extracted from this response, if any. |
| Complete | Whether the response was read completely. Complete is normal. TruncatedByCap means the response exceeded 8 MiB, TruncatedByTimeout means the server stopped sending, DirtyClose means the connection was closed before the response was complete. |
| TLS | Whether this hop was served over TLS. |
A report with a hop count of 0 means the probe could not complete the request for the control URL, for example
because DNS resolution failed or the connection timed out. If an entire cycle is missing, the probe most likely did not
receive a DHCP lease. Both cases are logged by nzyme-tap.
What to Look For
The value of portal integrity checks comes from comparing reports over time. The following indicators are worth checking regularly and after any change to the network:
- DHCP lease changes. A new DHCP server address, gateway address or DNS server address that nobody planned for points at a rogue DHCP server or a replaced gateway. Filter by probe name and sort by probe time to spot the first report with the new values.
- Resolved address changes. On a network with a captive portal, the control URL hostname usually resolves to the gateway (a site-local address) because the portal intercepts DNS. If it suddenly resolves to a public address, or to a public address in an unexpected ASN or country, DNS has been redirected.
- Final URL changes. The portal page moves to a different hostname, a different path, or a different domain. Pay special attention to domains that look like your vendor or like Microsoft, Google or Apple sign-in pages but are not.
- Hop count changes. An additional redirect inserted into the chain is a strong signal, especially if the new hop resolves to a public address.
- Missing TLS on the portal. If the final portal page used to be served over HTTPS and is now served over HTTP, or the other way around, something changed in the path.
- Content changes. Compare the raw content of the Presented Portal between reports. Look for injected
<script>tags, new form fields, "update required" or "verify your device" prompts, device code instructions, and download links. The CaptiveCrunch pages would have been visible here. - Unexpected success. If the control URL suddenly returns its clean response (for example
success) without any redirect, the captive portal is no longer enforcing sign-in for new clients. This may be intended, or it may be a misconfiguration that was introduced during an intrusion.
All report data is also available through the REST API, which allows you to export reports and compare them with your own tooling. See the API browser linked from the web interface for the available endpoints.
Retention
Portal integrity reports are stored in the Portal Integrity Checks data category. The default retention time is 360 days. You can change it per tenant as described in Retention Times. Note that reports contain the full raw response of every hop, so storage consumption depends on the size of the portal pages and on the configured interval and number of control URLs.
Limitations
- The probe records TLS certificates but does not validate them. Certificate validity has to be assessed from the recorded data.
- The probe does not complete the portal sign-in. It cannot observe what happens after a guest is authorized.
- The probe identifies itself with a
portal-integrity-probeuser agent. An attacker who selectively targets browser user agents would not serve malicious content to the probe. - IPv6 and protocols other than HTTP and HTTPS are not supported.