Unlocking free WiFi on British Airways
I was recently flying between HKG & LHR via British Airways. I’d done the same flight back in 2023, and remember relying on the in-flight entertainment for the 14 hour journey. However, this time on my way to London, they had an interesting offer: Free WiFi for “Messaging”, for members of “The British Airways Club”.
I was pretty sure I wasn’t a member of any sort of club (I’m only flying economy anyway); but turns out this is just the name of their frequent flyer program. Conveniently enough, you’re able to sign up for this via the captive portal while in the sky; and although it asks for your E-Mail you don’t need to verify it (thereby allowing you to complete the signup without access to the internet).
Once signed in, the captive portal invited me to “Start session”, which true to it’s word, let me start texting people. I tried Whatsapp, Signal, Wechat and Discord. The first three worked (though not for images), Discord expectedly did not. Not bad for free wifi!
How does it know?
This was the first question I had as soon as I confirmed messaging did work. It’s 2025; everything should be encrypted in transit. So how does it know if I’m using Whatsapp vs. Discord? One idea I had is it just somehow capped the bandwidth / data transfer of individual TCP connections; so when you’re sending a single message or two it gets through, but something larger would fail.
To test this, I used my phone to open up the classic: example.com. Unfortunately this didn’t load - so there must’ve been a bit more going on…
Thankfully I had my laptop on me, so the next step was to connect to WiFi with the devtools open to the network tab, and wireshark on the side for good measure. After registering for the WiFi again, it was time to play around a bit. Opening up something like example.com revealed a TCP reset in the wireshark, right after the Client Hello, and my brain immediately jumped to SNI. It’s something that’s really annoyed me about the TLS spec since it’s widely used by ISPs in India to block websites (although there is work being done to fix this; ECH (which was itself previously ESNI)).
tl;dr SNI reveals the domain name of EVERY website you connect to in the TLS handshake, before the tunnel is established! Although the actual contents of what you’re doing, on say, totallynondodgywebsite.com are encrypted, anyone on the wire can see that you connected to it (including ISPs). My guess was that they had a set of whitelisted domains used by messaging apps, and if they see anything else, they just reset the connection.
Sidebar: people’s reactions when I try to tell this are always extremely varied. Many of my non-technical friends think anything you do without a VPN is visible to everyone, while some slightly technical ones still think that the URL (including query params) is visible, but the responses are not. Finally there is some subset of people who believe TLS means all data is encrypted in transit between client & server, though they had no idea SNI leaks all the domains they visit!
Testing out the theory
Although BA blocks DNS queries to all (well all I could remember) public resolvers, they do resolve any domain you throw at them, including MX, TXT, HTTPS records. (This itself could be an interesting area of exploration; especially since the DNS resolution can be triggered before signing up for free WiFi. Something along the lines of arbitrary subdomains which represent the request payload, and a custom nameserver that returns responses via the TXT record or something. Anyway…).
Getting the A record of my personal server, I made a TLS handshake to the IP address directly, without any SNI. This was then reset by BA; so the lack of SNI is also blocked!
$ openssl s_client -connect 95.217.167.10:443
Connecting to 95.217.167.10
CONNECTED(00000003)
write:errno=104
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 302 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Protocol: TLSv1.3
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
The next step was to try and test some SNI that might go through. Off the top of my head, I knew wa.me
was used by Whatsapp for some stuff, so I decided to use it. The way SNI works is it tells the server which host you want to connect to, so it can present the right TLS certificate. In my case, my server did not have any cert for wa.me
, but NGINX seemingly just ignores the SNI if it doesn’t exist and returns the first cert (I think; could also be related to my config but I didn’t look to much into this).
But basically, as long as I (the client) don’t care, I can complete the TLS connection for any random cert the server offers me, even if in the SNI I provide a domain I don’t control (e.g. wa.me
in this case).
$ openssl s_client -connect 95.217.167.10:443 -servername wa.me
Connecting to 95.217.167.10
CONNECTED(00000003)
depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1
verify return:1
depth=1 C=US, O=Let's Encrypt, CN=R3
verify return:1
depth=0 CN=mijia.mywaifu.best
verify error:num=10:certificate has expired
notAfter=Jul 22 13:03:02 2023 GMT
verify return:1
depth=0 CN=mijia.mywaifu.best
notAfter=Jul 22 13:03:02 2023 GMT
verify return:1
---
Certificate chain
<snip>
Success! Using a Whatsapp SNI tricked BA into thinking I’m “messaging”, which allowed the TLS tunnel to be established. Since I am connected to the server, to make sure it works I wrote an HTTP/1.1 request within the socket; using the host header of a real website on my NGINX instance
GET / HTTP/1.1
Host: saxrag.com
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 09 May 2025 19:14:46 GMT
Content-Type: text/html
Content-Length: 4968
Last-Modified: Wed, 09 Apr 2025 07:52:54 GMT
Connection: keep-alive
ETag: "67f62756-1368"
Cache-Control: no-cache
Accept-Ranges: bytes
<snip>
I successfully managed to request and receive my homepage! All ~5KiB of it, not bad. Now the challenge was to extend this to browse any website…
Enemies to Lovers
Ok, my relationship with SNI isn’t as cliche as that, and I think we’re still enemies. But this opens up some exciting opportunities to say the least. If I can convince BA that I’m connecting to wa.me
, I can potentially do whatever I want over that connection (under the guise of “messaging”). So the requirments were:
- Establish a TLS connection using the SNI
wa.me
- Tunnel arbitrary traffic through that connection
- Do all this without actually owning / controlling the
wa.me
domain
From my past experiences w/ reverse-engineering etc., the most obvious way to do this seemed to be an HTTPS proxy. It had to be HTTPS specifically, since the connection to proxy was going to be what I’d “fake” as Whatsapp. If the TLS handshake to the HTTPS proxy had the SNI wa.me
, BA should let it through, and then I can make the real requests I want via the proxy.
Unfortunately I was in the air, and without easy access to the internet to manage my servers and the like, I couldn’t quite set all of this up; I’d have to do that while on holiday and test it on the flight back. I could try and emulate the BA restrictions etc. while on thr ground, but I decided to YOLO it.
The Setup
I managed to find one of my VPSs that wasn’t already using port 443. Let’s assume the public IP was 333.333.333.333
(yes I know octets don’t go beyond 0xFF
, if you really want my IP check the screenshots below). I then setup an HTTP proxy on it using tinyproxy. However this just sets up a basic HTTP proxy, which was listening on 127.0.0.1:8080
.
To add the TLS layer, I used stunnel. For the TLS setup of stunnel, I just generated some self-signed certs via openSSL using all defaults, except the common name (CN), for which I used wa.me
, since I wanted to try and ensure max compatibility (e.g. the client doesn’t reject due to unexpected SNI vs. CN, or the server not knowing which cert to provide).
openssl req -nodes -newkey ed25519 -keyout ssl.key -x509 -days 365 -out ssl.crt
UPDATE: actually, on the client I decided to ignore TLS errors (self-signed cert), and stunnel didn’t care about SNI, so this (CN
) didn’t matter too much. But for more legit use cases it definitely does!
Testing it out
To just make sure the proxy worked as expected, I tried it via curl directly on the IP:
$ curl -x https://user:pass@333.333.333.333:443 ifconfig.co -v
* Trying 333.333.333.333:443...
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self-signed certificate
* closing connection #0
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
Of course! I just randomly generated the certs on my VPS, not signed by a “trusted” CA or anything. Well, we can tell cURL to ignore TLS errors for the proxy with the --proxy-insecure
flag, and now it works; the response is the IP of my VPS.
However there’s a problem - if I connect to the proxy directly via the IP, there is no SNI extension set, so this would get blocked. The SNI extension is set when connection to a domain, so I need to configure wa.me
to point to 333.333.333.333
. This can be done via the hosts file of course, but cURL also provides a quick CLI hack via --resolve
:
curl --resolve wa.me:443:333.333.333.333 -x https://username:password@wa.me ifconfig.co --proxy-insecure -v
This tells cURL how to resolve the IP. With this, I could now see the SNI being set to wa.me
via wireshark, and the connection to the proxy succeeding (TLS errors about the self-signed cert ignored of course). Not bad, now time to wait for my flight back to Hong Kong…
Testing it in-flight
If I’d messed something up, I was cooked, since without internet access I wouldn’t be able to fix it! My flight back was at 1935hrs local time, but I’d been up since 0400 thanks to an early morning flight in from Edinburgh, and then spent the day browsing the markets, having pints and watching the Emilia Romagna Grand Prix. The place I went to even had screens above the urinals!
Anyway, despite being up for ~16 hours already, I was ready to see if my work would, well, work. Once we were sky-high, I connected to the WiFi (from my laptop), signed up for the BA loyalty program, and activated the “Messaging” plan. Trying the curl command from above, I got back an HTTP 200 from ifconfig.co
with my VPS IP; it worked! For good measure I tried cURLing some more websites like example.com, google.com etc. to make sure stuff seemed fine.
The next challenge was to extend this to web browsing. Thankfully most modern browsers support sending traffic through an HTTPS proxy, and chromium even has a flag to disable TLS cert warnings (so it won’t complain about my self-signed cert, which obviously doesn’t belong to the real wa.me
).
I also had to set a DNS record for wa.me
to 333.333.333.333
in my hosts file, so chromium would set the SNI to wa.me
in the TLS handshake, but the connection would be made to my VPS. Since the bandwidth would probably be quite limited (owing to not just the internet on an airplane, but proxying it via a VPS in Netherlands), I decided to load a very simple, text-heavy website: Hacker News.

Bada bing bada boom! Looks like we cooked. I can actually browse HN using BA’s free “messaging” WiFi! (Note: the reason you can see the HTTP requests in plaintext in wireshark is because I used SSLKEYLOGFILE and configured wireshark to decrypt TLS).
Unfortunately, trying to load websites with heavier assets would fail, with images on simple text blogs loading line-by-line. Well, at least its some dial-up nostalgia!

My guess is that on the free WiFi, apart from the SNI checks, they also throttle the bandwidth. Maybe they anticipated this kind of circumvention. On the other hand, if this is really the internet speed that the full plan unlocks…
Bonus: ECH
Earlier above I talked about work being done to fix the SNI leakage: ECH. The scope of explaining how it works is out of scope here, but I do encourage you to read up on it. Pretty good stuff! It’ll help this section make more sense.
I operate an ECH test website, so I decided to do some more setup before my flight. I basically created another ECHConfig, with the public_name set to wa.me
. I’ve a bit of a guide on how to do this btw, though it could do with some improvements.
Anyway, since ECH world, the public SNI is purely for the server to complete the outer ClientHello, and since ECH clients set the public SNI based on the ECHConfig, I can type in my real domain in firefox, which will still use the wa.me
domain as the public SNI. The inner Client Hello will then occur securely, containing the real SNI of rfc5746.mywaifu.best
, and the handshake will complete with the “legit” CA-signed certificate for that domain.

This worked as well, and without any TLS ignore flags, since the actual cert for rfc5746.mywaifu.best
was signed by a “trusted CA” (Let’s Encrypt). What’s more interesting is that this worked even on a non-standart TLS port: 7443
! Not sure exactly why, but I’m not complaining.
A note on ECHConfig resolution
Typically, ECHConfigs should be resolved via encrypted DNS, such as DNS-over-HTTPS. I believe this is what firefox does by default. I am not 100% sure if this is what happened while I was in flight, since I’d think the DoH would be blocked on messaging WiFi? Or maybe they allow the DoH SNI as well, since newer phones default to that. If any of you are flying BA anytime soon, try it out and let me know!
SNI: Don’t blindly trust it
SNI, as the name indicates (sorry) is just a “hint” of sorts, from the client to the server. If someone controls both sides (client & server), they can put whatever fake value they want in here, for middleboxes to sniff out and try to analyze. While this unfortunately does work for applications like censorship (where an ISP or country is trying to block a particular website), for use cases such as threat detection it should not be relied on; malwre authors can “spoof” the SNI when connecting to their C&C, since they don’t actually need it, but it may look more innocent to middleboxes.
Questions?
I would be happy to answer any questions you have! You can contact me via email, and please use my PGP key to encrypt all communications.