Getting free internet on a cruise, saving $170

6 hours ago 1

Picture this, you’re a teenager in the middle of the ocean on a cruise ship. All is good, except you’re lacking your lifeblood: internet. You could pay $170 for seven days of throttled internet on a single device, and perhaps split it via a travel router or hotspot, but that still seems less than ideal.

I’ve been travelling Europe with family and am currently on Princess Cruises’ Sun Princess cruise ship. For the past two days, the ship has mostly been in port, so I was able to use a cellular connection. This quickly became not feasible as we got further from land, where there was no coverage. At around the same time, I wanted to download the Princess Cruises app on my phone, and realized that it would give me a one-time 15-minute internet connection. I activated it, and quickly realized that it didn’t limit you to just the Play Store or App Store: all websites could be accessed. I soon realized that this “one-time” download was MAC-address dependent and thus, could be bypassed by switching MAC addresses. However, doing so logs you out of the MedallionNet portal, which is required to get the 15-minutes of free internet.

This means that in order to get just 15 minutes of internet, the following is required:

  1. Change MAC address
  2. Login to MedallionNet with date-of-birth and room number, binding the MAC address to your identity
  3. Send a request with your booking ID activating 15 minutes of free internet, intended to be used for downloading the Princess app
    • Not completely sure, but it did initially seem that to activate unrestricted access to the internet, you would need to send a simple HTTP to play.google.com, although I don’t think this is needed.

This process, on the surface, seems extremely arduous. But if this could be automated, it would suddenly become a much more viable proposition. After looking at the fetch requests the MedallionNet portal was sending to login and activate the free sessions, I realized that it shouldn’t be too hard to automate.

Conveniently, my family also brought a travel router running OpenWRT as we were planning on purchasing the throttled single-device plan (which is ~$170 for the entire cruise) and using the router as a hotspot so we could connect multiple devices to the internet. This router (a GL.iNet) allows you to change the MAC address via the admin portal, needing only a single POST request. This meant that if I could string together the API requests to change the MAC address (after getting a token from the router login endpoint), login to MedallionNet, and request the free internet session, I would have free internet.

I first tried copying the requests as cURL commands via DevTools into Notepad and using a local LLM to vibe-code a simple bash file. Realizing this wasn’t going to work, I began work on a Python script instead. I converted the cURL commands to requests via Copilot (yes, I used LLMs to an extent when building this) and started chaining together the requests.

The only issues I faced that took some time to overcome were figuring out how to repeat the requests when needed and being resistant to unexpected HTTP errors. For the former, I initially tried repeating it on an interval (first through a while True loop and later via shell scripting in the container) but later realized it was much easier by checking if internet access expired by sending a request to example.com and checking if it fails. For the latter, I used while True loops to allow the requests to be retried and executed break when the requests succeeded. The only other issue, that still exists, is that occasionally, the connection will drop out while the session is refreshed, although this seems to happen less than every 15 minutes and only lasts for a minute or two.

The container running in Docker Desktop, refreshing the session when needed

The container running in Docker Desktop, refreshing the session when needed

After comparing speeds with another guy I met on the cruise (who also happens to be a high-school programmer), who had the highest-level MedallionNet plan, there seems to be no additional throttling (7+ Mbps). Even better, after connecting my power bank’s integrated USB-C cable to the router, I’m able to move around the ship for hours. So far, I’ve been using the router for nearly ~7 hours and my 10,000 mAh power bank is still at 42% battery. In fact, I’m writing this very post while connected to the router.

The script

Here’s the code that I’ve been using and a sample .env, although I have a repository with the Dockerfile and compose file as well. Also, I’m aware the code isn’t pretty, I mostly just wrote it as a proof-of-concept that turned out to work amazingly well. I also doubt I’ll update the code after my cruise ends, since it won’t really be possible to test it. If you try to use this later, and it’s broken, you can go to DevTools, go to the network tab, go to “Fetch/XHR”, and then reload, which should allow you to reverse-engineer the API if you want to try to fix the script.

FIRST_NAME=YourFirstName LAST_NAME=YourLastName DOB=YYYY-MM-DD BOOKING_ID=YourBookingID ROOM_NUMBER=YourRoomNumber PASSWORD=YourPassword
# Not doing uv's script syntax import time import requests from rich.pretty import pprint import os from dotenv import load_dotenv import random from netaddr import EUI, mac_unix_expanded load_dotenv() # ONLY_RUN_ONCE = False FIRST_NAME = os.getenv("FIRST_NAME") LAST_NAME = os.getenv("LAST_NAME") DOB = os.getenv("DOB") BOOKING_ID = os.getenv("BOOKING_ID") PASSWORD = os.getenv("PASSWORD") ROOM_NUMBER = os.getenv("ROOM_NUMBER") USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1" def app_access() -> bool: url = "https://mnet.su.ocean.com/captiveportal/api/v1/pcaAppDownload/usr/appValidAccess" headers = { "accept": "application/json", "accept-language": "en", "apicaller": "3", "content-type": "application/json", "origin": "https://mnet.su.ocean.com", "priority": "u=1, i", "referer": "https://mnet.su.ocean.com/MednetWifiWeb/plan", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "sec-gpc": "1", "user-agent": USER_AGENT } data = { "bookingId": BOOKING_ID } response = requests.post(url, headers=headers, json=data) try: pprint(response.json()) if response.json()["isTimeRemaining"] is False: return True except Exception as e: pprint({"error getting internet access via app download method": str(e)}) return # Make a simple request to https://play.google.com or apple app store time.sleep(5) # Small delay since I don't think it gives access immediately try: url = "https://apps.apple.com/us/app/princess-cruises/id6469049279" # url = "https://play.google.com" headers = { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "accept-language": "en-US,en;q=0.5", "user-agent": USER_AGENT } response = requests.get(url) pprint( response.status_code, ) except requests.exceptions.ConnectionError: pprint("Couldn't GET app store url, should be fine though") return False def login_user(ship_code: str): url = "https://mnet.su.ocean.com/captiveportal/api/v1/loginUser" headers = { "accept": "application/json", "accept-language": "en", "apicaller": "3", "content-type": "application/json", "origin": "https://mnet.su.ocean.com", "priority": "u=1, i", "referer": "https://mnet.su.ocean.com/MednetWifiWeb/login", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "sec-gpc": "1", "user-agent": USER_AGENT } data = { "cabinNumber": ROOM_NUMBER, "dob": DOB, "clickfromOceanConcierge": False, "shipCode": ship_code, "tokenType": 1 } response = requests.post(url, headers=headers, json=data) # pprint(response.json()) def random_mac_even_first_byte() -> str: # Generate a random MAC with even first byte first_byte = random.randint(0, 255) & 0xFE mac_bytes = [first_byte] + [random.randint(0, 255) for _ in range(5)] mac = EUI(':'.join(f"{b:02x}" for b in mac_bytes)) mac.dialect = mac_unix_expanded return str(mac) def edit_mac(admin_token): url = "http://192.168.8.1/cgi-bin/api/router/mac/clone" new_mac = random_mac_even_first_byte() # Use the correct function headers = { "Accept": "application/json, text/javascript, */*; q=0.01", "Accept-Language": "en-US,en;q=0.8", "Authorization": admin_token, "Connection": "keep-alive", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Origin": "http://192.168.8.1", "Referer": "http://192.168.8.1/", "Sec-GPC": "1", # "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "User-Agent": USER_AGENT, "X-Requested-With": "XMLHttpRequest" } cookies = { "Admin-Token": admin_token } data = { "newmac": new_mac } response = requests.post(url, headers=headers, cookies=cookies, data=data, verify=False) pprint({"new_mac": new_mac, "response": response.json()}) return response.json() def login_router(password): """ Logs into the router and returns the Admin-Token. """ url = "http://192.168.8.1/cgi-bin/api/router/login" headers = { "Accept": "application/json, text/javascript, */*; q=0.01", "Accept-Language": "en-US,en;q=0.8", "Authorization": "undefined", "Connection": "keep-alive", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Origin": "http://192.168.8.1", "Referer": "http://192.168.8.1/", "Sec-GPC": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "X-Requested-With": "XMLHttpRequest" } data = { "pwd": password } response = requests.post(url, headers=headers, data=data, verify=False) # pprint({"status_code": response.status_code}) # pprint({"headers": dict(response.headers)}) # pprint({"cookies": response.cookies.get_dict()}) try: token = response.json().get("token") # pprint({"login_router_response": response.json(), "Admin-Token": token}) return token except Exception as e: pprint({"error": str(e)}) return None def can_connect_to_example() -> bool: try: requests.get("https://example.com", timeout=5) return True except requests.RequestException: return False def get_ship_code() -> str: # https://mnet.su.ocean.com/captiveportal/api/v1/shipcodes/ship # {"shipCodesMapping":[{"shipCode":"SU","shipName":"Sun Princess"}]} url = "https://mnet.su.ocean.com/captiveportal/api/v1/shipcodes/ship" headers = { "accept": "application/json", "accept-language": "en", "content-type": "application/json", "user-agent": USER_AGENT } response = requests.get(url, headers=headers) try: ship_code = response.json()["shipCodesMapping"][0]["shipCode"] return ship_code except Exception as e: pprint({"error getting ship code, defaulting to SU for Sun Princess": str(e)}) return "SU" def connect_to_internet(ship_code: str): while True: # Loop until we get internet access while True: # Loop until we get any response from the app internet request while True: # Loop until we successfully login print("Logging in") try: login_user(ship_code=ship_code) except requests.exceptions.ConnectionError: print("Couldn't connect to login page, sleeping for 2 seconds and retrying") time.sleep(2) continue else: break print("Requesting app internet") try: # Don't regen mac unless this returns true print("Randomizing MAC due to session expiration") regen_mac = app_access() except requests.exceptions.ConnectionError: print("Couldn't get app internet, sleeping for 2 seconds and retrying") time.sleep(2) continue else: break if regen_mac: edit_mac(login_router(PASSWORD)) # time.sleep(3) else: break def main(): print("hi") ship_code = get_ship_code() while True: if can_connect_to_example(): print("Internet access detected (can connect to example.com). Waiting 15 seconds before checking again.") time.sleep(15) continue else: print("Cannot connect to example.com, attempting to activate a 15 minute free cruise internet session") connect_to_internet(ship_code=ship_code) print(f"Finished at {time.strftime('%H:%M:%S')}, but sleeping for 30 seconds before verifying internet access") time.sleep(30) # Sleep for 30 seconds before checking again since it might take a while for internet access to be granted if __name__ == "__main__": main()
Read Entire Article