Captive Portal works on iOS, fails on Android (All standard solutions attempted)

Hello everyone,

I'm opening this new thread for an issue that I haven't been able to solve, even after trying all the documented solutions found here on the forums and elsewhere. Any insights would be greatly appreciated.

Setup Details:

  • Model: hAP ax^3 (C53UiG+5HPaxD2HPaxD)
  • RouterOS Version: 7.19.6 (upgraded from 7.19.4 while trying to solve this issue).

Problem Summary: I have a Hotspot with a captive portal that works perfectly on iOS devices (iPhone). When connecting to the guest network, the portal pops up automatically without any issues.

However, on Android devices (tested on multiple models and versions), the captive portal does not launch automatically. The device remains connected with a "Connected, no internet" status. To get access, the user must manually open a browser and navigate to the gateway IP (192.168.20.1) or the DNS name (login.imprepide.com).

Troubleshooting Steps Taken (Without Success):

  • DHCP & DNS Configuration: The explicit DNS server configuration was removed from the Hotspot's network settings to allow the service to properly handle redirects.
  • HTTPS Redirect Investigation: It was confirmed that HTTPS redirection is a common cause of issues on Android.
    • The HTTPS Redirect option was missing from the GUI (WinBox/WebFig) on both 7.19.4 and 7.19.6.
    • Discovered that this is intentional behavior in RouterOS 7 and is now controlled by assigning (or not assigning) an SSL certificate.
    • The Hotspot profile is now configured with ssl-certificate=none to prevent HTTPS redirection, as per current best practices.
  • Android-Specific Fix: A static DNS entry was added to the router to force portal detection on Android. The entry exists and is configured correctly:
    • name=connectivitycheck.gstatic.com address=192.168.20.1
  • Client-Side Diagnostics (Android): All recommended tests were performed on the Android client device.
    • Confirmed the device is connected to the correct Wi-Fi network.
    • Confirmed the device receives a valid IP address from the Hotspot's DHCP pool (192.168.20.xxx).
    • The "Private DNS" feature was disabled on the phone.
    • A clean test was performed: "Forgetting" the network on the phone and rebooting the device completely before reconnecting.

Despite all of these steps, the behavior persists: iOS works, Android fails.

[admin@MKT-ImprePIDE-PZO] > / export

2025-09-23 21:24:31 by RouterOS 7.19.6

software id = 5SBJ-LU26

model = C53UiG+5HPaxD2HPaxD

serial number = xxxxxxxxx

/interface bridge
add name=bridge1 vlan-filtering=yes
/interface ethernet
set [ find default-name=ether5 ] name=GESTION
set [ find default-name=ether1 ] name=ISP
set [ find default-name=ether3 ] name=LIBRE
set [ find default-name=ether4 ] name=NVR
set [ find default-name=ether2 ] name=SWITCH
/interface wireguard
add comment="WG - Admin remoto" disabled=yes listen-port=51820 mtu=1420 name=wg0
/interface vlan
add interface=bridge1 name=vlan10 vlan-id=10
add interface=bridge1 name=vlan20 vlan-id=20
add interface=bridge1 name=vlan30 vlan-id=30
add interface=bridge1 name=vlan99 vlan-id=99
/interface list
add comment="Puertos trunk 802.1Q" name=TRUNK
add comment="salida a internet" name=WAN
/interface wifi security
add authentication-types="" disabled=no name=sec-open
add authentication-types=wpa2-psk,wpa3-psk disabled=no name=sec-privado
/interface wifi configuration
add mode=ap name=cfg-privado security=sec-privado ssid="ImprePIDE - PZO"
add mode=ap name=cfg-invitados security=sec-open ssid="ImprePIDE - Invitados"
/interface wifi
set [ find default-name=wifi1 ] channel.band=5ghz-ax configuration=cfg-privado disabled=no
add configuration=cfg-invitados disabled=no mac-address=F6:1E:57:0B:FD:E0 master-interface=wifi1 name=wifi1-invitados
set [ find default-name=wifi2 ] channel.band=2ghz-ax configuration=cfg-invitados disabled=no
add configuration=cfg-privado disabled=no mac-address=F6:1E:57:0B:FD:E1 master-interface=wifi2 name=wifi2-privado
/ip hotspot profile
add dns-name=login.imprepide.com hotspot-address=192.168.20.1 login-by=http-pap name=hs-guest
/ip hotspot user profile
add add-mac-cookie=no idle-timeout=1h keepalive-timeout=1m !mac-cookie-timeout name=guest-1h rate-limit=15M/10M shared-users=50
/ip pool
add name=pool_vlan99 ranges=192.168.99.100-192.168.99.199
add name=pool_vlan20 ranges=192.168.20.50-192.168.20.200
add name=pool_vlan30 ranges=192.168.30.100-192.168.30.199
add name=pool_vlan10 ranges=10.0.0.100-10.0.0.199
/ip dhcp-server
add address-pool=pool_vlan99 interface=vlan99 name=dhcp_vlan99
add address-pool=pool_vlan10 interface=vlan10 lease-time=1d name=dhcp_vlan10
add address-pool=pool_vlan20 interface=vlan20 name=dhcp_vlan20
add address-pool=pool_vlan30 interface=vlan30 lease-time=1d name=dhcp_vlan30
/ip hotspot
add address-pool=pool_vlan20 addresses-per-mac=1 disabled=no interface=vlan20 keepalive-timeout=10m name=hs-guest profile=hs-guest
/user group
add name=restonly policy=read,write,rest-api,!local,!telnet,!ssh,!ftp,!reboot,!policy,!test,!winbox,!password,!web,!sniff,!sensitive,!api,!romon
add name=hotspot-api policy=read,write,api,!local,!telnet,!ssh,!ftp,!reboot,!policy,!test,!winbox,!password,!web,!sniff,!sensitive,!romon,!rest-api
/certificate settings
set builtin-trust-anchors=not-trusted
/interface bridge port
add bridge=bridge1 interface=SWITCH pvid=10
add bridge=bridge1 interface=LIBRE pvid=10
add bridge=bridge1 interface=NVR pvid=30
add bridge=bridge1 interface=GESTION pvid=99
add bridge=bridge1 interface=wifi1 pvid=10
add bridge=bridge1 interface=wifi1-invitados pvid=20
add bridge=bridge1 interface=wifi2-privado pvid=10
add bridge=bridge1 interface=wifi2 pvid=20
/interface bridge vlan
add bridge=bridge1 tagged=bridge1 untagged=SWITCH,LIBRE,wifi1,wifi2-privado vlan-ids=10
add bridge=bridge1 tagged=bridge1 untagged=wifi2,wifi1-invitados vlan-ids=20
add bridge=bridge1 tagged=bridge1 untagged=NVR vlan-ids=30
add bridge=bridge1 tagged=bridge1 untagged=GESTION vlan-ids=99
/interface list member
add interface=ISP list=WAN
/interface wireguard peers
add allowed-address=10.0.250.2/32 comment=iPhone disabled=yes interface=wg0 name=peer1 persistent-keepalive=25s public-key=""
/ip address
add address=192.168.20.1/24 interface=vlan20 network=192.168.20.0
add address=192.168.30.1/24 interface=vlan30 network=192.168.30.0
add address=192.168.99.1/24 interface=vlan99 network=192.168.99.0
add address=10.0.0.1/24 comment="Privada (VLAN10)" interface=vlan10 network=10.0.0.0
add address=10.0.250.1/24 comment="WG subnet" interface=wg0 network=10.0.250.0
/ip cloud
set ddns-enabled=yes update-time=no
/ip dhcp-client
add interface=ISP use-peer-dns=no
/ip dhcp-server network
add address=10.0.0.0/24 dns-server=1.1.1.1,8.8.8.8 gateway=10.0.0.1
add address=192.168.20.0/24 gateway=192.168.20.1
add address=192.168.30.0/24 dns-server=1.1.1.1,8.8.8.8 gateway=192.168.30.1
add address=192.168.99.0/24 dns-server=8.8.8.8 gateway=192.168.99.1
/ip dns
set allow-remote-requests=yes servers=8.8.8.8,1.1.1.1
/ip dns static
add address=192.168.20.1 name=connectivitycheck.gstatic.com type=A
/ip firewall address-list
add address=10.0.0.0/24 comment=Privada list=WG-ALLOWED
add address=192.168.30.0/24 comment=NVR list=WG-ALLOWED
add address=192.168.99.0/24 comment=Gestin list=WG-ALLOWED
/ip firewall filter
add action=passthrough chain=unused-hs-chain comment="place hotspot rules here" disabled=yes
add action=accept chain=input comment="INPUT established/related" connection-state=established,related
add action=drop chain=input comment="INPUT drop invalid" connection-state=invalid
add action=accept chain=input comment="Mgmt via WG" in-interface=wg0
add action=accept chain=input comment="WireGuard desde Internet" dst-port=51820 in-interface=ISP protocol=udp
add action=accept chain=input comment="API para Apps Script" dst-port=443 in-interface=ISP protocol=tcp
add action=drop chain=input comment="Bloquear acceso remoto desde WAN" in-interface=ISP
add action=accept chain=forward comment="FWD established/related" connection-state=established,related
add action=drop chain=forward comment="FWD drop invalid" connection-state=invalid
add action=accept chain=forward comment="WG -> LAN permit" dst-address-list=WG-ALLOWED src-address=10.0.250.0/24
add action=drop chain=forward comment="Isolate VLAN10 -> VLAN20" in-interface=vlan10 out-interface=vlan20
add action=drop chain=forward comment="Isolate VLAN20 -> VLAN10" in-interface=vlan20 out-interface=vlan10
add action=drop chain=forward comment="Isolate VLAN10 -> VLAN30" in-interface=vlan10 out-interface=vlan30
add action=drop chain=forward comment="Isolate VLAN30 -> VLAN10" in-interface=vlan30 out-interface=vlan10
add action=drop chain=forward comment="Isolate VLAN20 -> VLAN30" in-interface=vlan20 out-interface=vlan30
add action=drop chain=forward comment="Isolate VLAN30 -> VLAN20" in-interface=vlan30 out-interface=vlan20
add action=drop chain=forward comment="Guests -> Mgmt drop" in-interface=vlan20 out-interface=vlan99
/ip firewall nat
add action=passthrough chain=unused-hs-chain comment="place hotspot rules here" disabled=yes
add action=masquerade chain=srcnat comment="NAT nico" out-interface-list=WAN
/ip hotspot user
add name=guest profile=guest-1h server=hs-guest
/ip hotspot walled-garden
add dst-host=forms.gle
add dst-host=script.google.com
add dst-host=docs.google.com
add dst-host=ssl.gstatic.com
add dst-host=.googleusercontent.com
add dst-host=
.googleapis.com
add dst-host=.gstatic.com
add dst-host=imprepide.com
add dst-host=www.imprepide.com
add dst-host=
.imprepide.com
/ip service
set ssh disabled=yes
set telnet disabled=yes
set www address=192.168.99.0/24,10.0.0.0/24
set www-ssl address=192.168.99.0/24,10.0.0.0/24 disabled=no
set api-ssl address=192.168.99.0/24,10.0.250.0/24 disabled=yes port=443
set winbox address=192.168.99.0/24,10.0.0.0/24
set api disabled=yes
/system clock
set time-zone-autodetect=no time-zone-name=America/Caracas
/system identity
set name=MKT-ImprePIDE-PZO
/system logging
add topics=hotspot
/system ntp client
set enabled=yes
/system ntp client servers
add address=time.cloudflare.com
add address=time.google.com
add address=pool.ntp.org
/tool netwatch
add comment="Health check nico" down-script=":log warning "Internet caido (sin respuesta de 8.8.8.8)";" host=8.8.8.8 interval=30s timeout=3s type=simple up-script=
":log info "Internet OK (8.8.8.8 responde)";"
[admin@MKT-ImprePIDE-PZO] >

The Question: Given that the configuration appears to be 100% correct, follows recommended best practices, and works for one platform (iOS), what could be causing it to fail specifically on Android? Is there a known bug with the combination of RouterOS 7.19.6 and the hAP ax^3 hardware that could affect this? Are there any non-standard Firewall/NAT or Mangle rules that might be required to resolve this?

Any ideas or suggestions would be very welcome. Thanks in advance.

No this is not the "best practices" at all. Just let the router get a Let's Encrypt certificate (if you don't own any domain, use the xxxxxx.sn.mynetname.net that MikroTik provides to you with IP -> Cloud). Use this domain as your hotspot domain too and set ssl-certificate to the issued Let's Encrypt certificate. Please note that it doesn't matter that outside on the Internet the domain points to your public IP address, and not your hotspot IP address. Inside the Hotspot, the DNS is automatically manipulated so that the domain points to the hotspot IP.

With a TLS certificate and HTTPS turned on, RFC 7710 support will automatically be activated by RouterOS, see:

And Android honors this since version 11: Captive portal API support | Android Developers.

Without HTTPS, none of that works!

Please note that if you use the LE certificate, you'll need to schedule some scripting so that every 80 days or so the renewed certificate is correctly set in you Hotspot setting (if you don't want to do that manually).

If you have your own domain, and don't want the domain to point to your public IP address for certificate renewal, then use the DNS-01 challenge method to issue the certificate. But there will be more manual steps involved if your domain provider has no API support.

As a side question, do you have some related resources/howto/examples about this?

Maybe it is just me, but Mikrotik help/documentation seems to be remarkably scarce around this matter.

Hi @jaclaz, which part were you referring to in your post? The part about setting the TLS certificate for Hotspot? Or the part about generating a Let's Encrypt certificate with the router?

I agree that the part about generating and renewing the LE certificates in RouterOS is not well documented at all. And the behavior of how the generated/renewed certificates are saved have changed many time since the LE support has been introduced in RouterOS.

There were RouterOS versions where the certificate names include a timestamp and change every time (and the chosen prefixes have changed with the versions too). Then there were versions where the domain name only is used as certificate name, but the previous certificate are not removed, so there were certificates with duplicated names in the certificate list, and when you select the certificate with the drop-down controls in various part of RouterOS, you can't even know which one is the older one and which one is the newer one. With recent versions they appear to remove the old certificate.

And when the certificate is renewed, most of the RouterOS versions only replaced the setting of the www-ssl service entry with the new certificate. If you happen to use the old LE certificates in other places, like User Manager/Hotspot/SSTP/etc... those entries would not be automatically updated and still point to the old certificate (or a removed certificate) and need to be manually fixed or updated with your own script.

Actually the whole stuff, I believe I can somehow manage to find out a way to get a an existing Let's Encrypt certificate working in the RoS, but how to use the Ip->Cloud to get one seems to me (as much of the hotspot related stuff) largely undocumented.

The overall project (with no deadline, I have all the time in the world) is this one:

basically a "Work in progress"/"Out of order" digital sign on the LAN.

If you have some related documentation/links and/or ideas (and of course the time) please post there.

Oh. As others have pointed out in that thread. It's doable with Hotspot (and Hotspot doesn't require WiFi, you can set it up on any VLAN) but only for clients using DHCP and the clients only see the portal page when the lease is 50% expires. Which means you'll need to shorten the DHCP lease time. And as you've also written, it doesn't help if the clients have their address and gateway information manually configured.

I really don't have any better idea for now.


Not related to your project, but to Let's Encrypt: In case you let your router get LE certificate and use a custom domain (not the xxx.sn.mynetname.net), the current requirement in RouterOS is that the www service must be running, and port TCP 80 must be opened on the input chain. If you are like me and usually have both disabled, and you also have different parts in RouterOS that use the LE certificate (not only www-ssl), you can't rely on RouterOS to automatically renew the certificate. I use a (scheduled) script for it as below:

Script: update_letsencrypt
:local domainName "my.domain.com";
:local fwComment "allow Let's Encrypt";

:log info "Requesting LE certificate renewal for $domainName..."

# temporarily open port 80 on the firewall and enable WWW service
/ipv6 firewall filter enable [find where comment=$fwComment];
# /ip firewall filter enable [find where comment=$fwComment];
/ip service enable [find name="www" !dynamic];

:local leLog [/certificate enable-ssl-certificate dns-name=$domainName as-value];

# close port 80 on the firewall and disable WWW service
/ip service disable [find name="www" !dynamic];
# /ip firewall filter disable [find where comment=$fwComment];
/ipv6 firewall filter disable [find where comment=$fwComment];

:log info $leLog;
:log warning "LE certificate renewed for $domainName!";

:local leCertName [/ip/service/get [find name="www-ssl" !dynamic] certificate];
:local minExpire 1d;
:local certFingerprint "";

# find certificate to keep
:foreach leCert in=[/certificate find name=$leCertName] do={ 
    :local eaf [/certificate get $leCert expires-after];
    :if ($eaf > $minExpire) do={
        :set minExpire $eaf;
        :set certFingerprint [/certificate get $leCert fingerprint];
    };
};

:foreach leCert in=[/certificate find name=$leCertName fingerprint!=$certFingerprint] do={ 
    :local eaf [/certificate get $leCert expires-after];
    :log warning "Removing old certificate with EA $eaf...";
    /certificate remove $leCert;
};

:log info "Updating Hotspot to use \"$leCertName\"...";
/ip hotspot profile set [find dns-name=$domainName] ssl-certificate=none;
/ip hotspot profile set [find dns-name=$domainName] ssl-certificate=$leCertName;

:log info "Updating SSTP to use \"$leCertName\"...";
/interface sstp-server server set certificate=none;
/interface sstp-server server set certificate=$leCertName;

:log info "Updating User Manager to use \"$leCertName\"...";
/user-manager set certificate=none;
/user-manager set certificate=$leCertName;

The domain name $domainName needs to be adjusted. Also, a disabled firewall rule on the input chain needs to be present with the exact comment listed in $fwComment. And I only let the LE renewal use IPv6, if you use IPv4, you'll need to comment/uncomment the correct commands that enable and disable the firewall rule.

It deals with the issues I mentioned above, with the uncertainty of the name of the certificate, as well as whether the old certificate with the same name is automatically removed by RouterOS (the script tries to remove the old cert with the same name if such thing exists).

At the bottom of the script are commands that set the certificate for Hotspot/SSTP/User Manager. RouterOS updates the cert setting for www-ssl but not for the rest if you happen to also use the LE certificate for them. You can uncomment parts that you don't need.


As for using the LE certificate with Hotspot. It's just the matter of using the matching domain name (same as the one in the LE certificate) for dns-name of the Hotspot profile, and set ssl-certificate of the profile to the generated LE certificate. The field login-by can be set to cookie,http-chap,https.

As I wrote above, you can use any domain, even fake one not owned by you in the dns-name of the Hotspot profile and it will work for HTTP only login. But for HTTPS, you'll need a valid certificate (preferably not self-signed), so the domain must be a domain or subdomain owned by you.

If you request the LE certificate with the method above using the router with a subdomain owned by you, then the A or AAAA DNS record of that subdomain must of course be pointed to your router, because the HTTP-01 challenge is used.

But if you don't request the LE certificate using the router itself, then the subdomain needs no DNS record in the real world to exists at all. To generate the LE certificate, you'll need to use an external tool or service that support the DNS-01 challenge. This allow you to get LE certificates for any of your subdomain (the domain part must still be owned by you, you must be able to control it's DNS records), even '*.' certificates can be issued that way with Let's Encrypt. I use Posh-ACME for this. But this involves many manual steps and cannot be 100% automated on the router.

If instead of your custom domain, you use the one of IP -> Cloud then you don't need www running and don't need to temporarily open port 80, because MikroTik will use the DNS-01 challenge to get the LE certificate for you (they own the mynetname.net domain and DNS records).