Reolink cameras not no app, no cloud as advertised. How to fix it

Reolink cameras ship with every open-standard port switched off. What that means for a “no app, no cloud” purchase, and how to get in without the vendor’s software

Short version. I bought a Reolink Video Doorbell WiFi because the box and the website say it supports ONVIF and RTSP (the open standards that let any recorder or Home Assistant talk to a camera), needs no subscription, needs no account, and works without the internet. All true, eventually. But out of the box, every one of those open standards is switched off. The only thing the camera will talk to is Reolink’s own app or desktop program, over a private protocol Reolink does not document. The web-page setup that Reolink and the Home Assistant documentation both describe cannot work, because the web page is off too. And once the web page is forced on, its own Change Password form refuses to set a first password, because it demands an old one that does not exist.

There is a way through without the app, for both problems. It uses the same open-source code that Home Assistant itself uses to talk to Reolink cameras, plus one message borrowed from the Neolink project, and it takes about fifteen minutes. Everything you need is below, for Windows or Linux.

Who this is for

You bought a Reolink camera because you wanted a camera that stays on your own network and does not need a phone app, a vendor account, or the internet. You plugged it in, found its address, opened a browser, and got “connection refused.” This is for you.

If you are fine installing the Reolink app on your phone, you do not need this. The app works. The point of this article is that it is the only thing Reolink ships that does.

What I tested

Item Value
Camera Reolink Video Doorbell WiFi, black (item D340W), hardware DB_566128M5MP_W
Firmware as shipped v3.0.0.4662_2508071282 (build 2508071282, dated 2025-08-07)
Purchased 2026-08-31, new, sealed
How it was connected for setup Network cable from the doorbell’s Ethernet jack to the router. The router handed it an address automatically
Computer used Either works: Windows 10 with PowerShell and Python 3.13, or Linux Mint with Python 3.12. You need one, not both

To find your own camera’s hardware and firmware version once you are in: gear icon > System > Information. The hardware version is also printed on the box label.

What happens out of the box

The camera gets an address from the router. Typing that address into a browser fails. Checking which “doors” (ports) the camera has open shows this:

   80  closed     (web page, plain)
  443  closed     (web page, encrypted)
  554  closed     (RTSP: the standard video stream)
 8000  closed     (ONVIF: the standard camera-control protocol)
 9000  open       (Reolink's private protocol)

So the camera is alive and on the network, and every door that a standard tool could use is locked. The only open door is Reolink’s own.

Why this is a problem

Reolink says the doors are locked for safety. Fair enough on its own; plenty of devices ship locked down. The problem is what comes with it.

The setup instructions do not work. Reolink’s own help site says you can set up a Wi-Fi camera by plugging in a network cable and logging in through a browser. The Home Assistant documentation, which Reolink has officially endorsed, spells out the same method step by step: plug in the cable, find the address in your router, open it in a browser, set your password, enter your Wi-Fi details. On a new camera, none of that page exists. And Reolink’s own browser-access article quietly admits it: before you can use the browser, you have to log in with the Reolink app or client to open the ports. Two pages from the same company, and the one that a privacy-minded buyer relies on is the broken one.

The only key Reolink gives you is their own software. The switches that open the standard doors are inside the Reolink phone app and the Reolink desktop client. Their app setup expects your phone and the camera to both have internet access, and walks you toward a Reolink account and their remote-access relay. The desktop client is closed source. If you chose this camera specifically so you would never have to install vendor software, you find out after purchase that you were not given that option.

“ONVIF” and “RTSP” on the box describe doors that are locked. Those are not features you can check in the store. They are services on the camera that do not exist until Reolink’s software turns them on. A product that advertises open standards and ships with all of them disabled behind a private gate is not what the label says.

Getting the web page open is not the end of it. With the web page forced on, it logs you in with the empty password and shows the normal settings, not a first-run wizard. Its Change Password form has an Old Password box that will not accept nothing. Behind that form, the page talks to the camera over an encrypted version of its web API that Reolink does not document either, so the plain web command that every third-party tool uses to change a password is refused. The camera will let you in with no password and then will not let you set one, unless you use the vendor’s software or the same private protocol as before.

One more thing to know, because it matters for your safety. A brand-new camera has the user name admin and no password at all, and its private protocol accepts that. That is what makes the fix below possible. It also means that from the moment you power it on until you set a password, anyone on the same network can reconfigure it. Set it up on a network segment nothing else uses, and set the password as soon as you are in.

What Reolink should change

Any one of these would fix it:

  • Ship with the encrypted web page turned on, showing the same “set your password” screen the app shows. Every router and NAS does this.
  • Or ship with ONVIF turned on, since the ONVIF standard includes first-time user creation.
  • Or document the private protocol well enough that others can open the doors without guessing.
  • At the very least, fix the help pages so they stop offering a setup method that cannot work.

The fix, in plain terms

Home Assistant’s Reolink integration already knows how to handle a camera whose web door is locked. When that happens to a camera that used to work, the integration knocks on the private door (port 9000), asks the camera to unlock the web door, and carries on. The code that does this is a small open-source library called reolink_aio, written by the same person who maintains the Home Assistant integration. It is MIT-licensed, it is pure Python, and it talks only to the camera address you give it.

Home Assistant will not do this for a brand-new camera, because its setup form refuses an empty password. The library itself has no such rule. So the first script below asks the library to do exactly what Home Assistant does, with the empty password a new camera expects.

The second script sets the password the same way. The open-source Neolink project worked out the message Reolink’s own client sends to change a user’s password over the private protocol; the script sends that message through the same library, then logs back in with the new password to prove it took.

The library goes into one folder on your computer. When you are done, delete the folder and nothing is left behind.

Step 1: check which doors are open

Pick your operating system. Replace 192.168.1.50 with your camera’s address (find it in your router’s list of connected devices).

Windows (open PowerShell from the Start menu):

$ip = "192.168.1.50"
80,443,554,8000,9000 | ForEach-Object {
  $r = Test-NetConnection -ComputerName $ip -Port $_ -WarningAction SilentlyContinue
  "{0,5}  {1}" -f $_, $(if ($r.TcpTestSucceeded) {"open"} else {"closed"})
}

Linux (open a terminal):

ip=192.168.1.50
for p in 80 443 554 8000 9000; do
  if timeout 2 bash -c "</dev/tcp/$ip/$p" 2>/dev/null; then echo "$p open"; else echo "$p closed"; fi
done

If 9000 says open and 80 and 443 say closed, continue. If 9000 is closed too, the address is wrong.

Step 2: set up the tool

You need Python 3.11 or newer. Windows: type py --version in PowerShell; if it prints a version, you have it; if not, install it from python.org. Linux: it is already there.

Windows:

$w = "$env:USERPROFILE\Documents\reolink-init"
py -m venv $w
& "$w\Scripts\pip.exe" install -q reolink_aio

Linux:

python3 -m venv ~/reolink-init && ~/reolink-init/bin/pip install -q reolink_aio

Step 3: save the script

Copy the block below into a text editor and save it as reolink-openports.py inside the folder from step 2 (Documents\reolink-init on Windows, ~/reolink-init on Linux).

#!/usr/bin/env python3
# reolink-openports.py
# Version: 1.0 (2026-08-31)
# License: GPL-3.0-or-later
#
# Turns on the HTTP and HTTPS services (optionally RTSP, ONVIF, RTMP) of a
# Reolink camera that ships with them disabled, using the camera's native
# port-9000 protocol. Built on reolink_aio, the MIT-licensed library that the
# Home Assistant Reolink integration itself runs on. Talks only to the IP you
# give it. No vendor app, no vendor account, no internet.
#
# Usage:
#   python reolink-openports.py <camera-ip>
#   python reolink-openports.py <camera-ip> --ports http,https,rtsp,onvif
#   python reolink-openports.py <camera-ip> --password <pw>
#
# A factory-fresh camera has user "admin" and an empty password; leave
# --password off in that case.

import argparse
import asyncio

from reolink_aio.api import Host
from reolink_aio.baichuan.util import PortType

NAMES = ["http", "https", "rtsp", "onvif", "rtmp"]


def state(bc) -> str:
    return "  ".join(f"{n}={getattr(bc, n + '_enabled')}" for n in NAMES)


async def main(ip: str, password: str, ports: list[str]) -> None:
    h = Host(ip, "admin", password)
    try:
        await h.baichuan.get_ports()
        print("before:", state(h.baichuan))
        for p in ports:
            await h.baichuan.set_port_enabled(PortType[p], True)
        await h.baichuan.get_ports()
        print("after: ", state(h.baichuan))
    finally:
        await h.logout()


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description="Enable web/stream services on a Reolink camera over port 9000")
    ap.add_argument("ip", help="camera IP address")
    ap.add_argument("--password", default="", help="admin password (omit for a factory-fresh camera)")
    ap.add_argument("--ports", default="http,https", help="comma list from: " + ",".join(NAMES))
    a = ap.parse_args()
    wanted = [p.strip().lower() for p in a.ports.split(",") if p.strip()]
    bad = [p for p in wanted if p not in NAMES]
    if bad:
        ap.error(f"unknown port name(s): {', '.join(bad)}")
    asyncio.run(main(a.ip, a.password, wanted))

Step 4: run it

Windows:

& "$w\Scripts\python.exe" "$w\reolink-openports.py" $ip

Linux:

~/reolink-init/bin/python ~/reolink-init/reolink-openports.py $ip

You should see:

before: http=False  https=False  rtsp=False  onvif=False  rtmp=False
after:  http=True  https=True  rtsp=False  onvif=False  rtmp=False

Run the step 1 check again: 80 and 443 now say open. Open https:// followed by your camera’s address in a browser. The browser will warn that the security certificate is not trusted. That is normal for a device on your own network; proceed.

By default the script opens only the web doors. Leave RTSP and ONVIF closed until you know your recorder needs them; you can turn them on from the camera’s own settings page in step 6, or by running the script again with --ports rtsp,onvif.

Step 5: set the password

Opening https:// and your camera’s address now shows the camera’s settings page, and it lets you in with the empty password. Do not use its Change Password form; it will not accept an empty old password. Save the second script as reolink-setpassword.py in the same folder as the first, then run it. Choose a password of 6 to 32 characters using letters, digits, and only these symbols: @ $ * ~ _ - + = ! ? . , : ; ' ( ) [ ]. It must mix at least two of: lowercase, uppercase, digits, symbols.

#!/usr/bin/env python3
# reolink-setpassword.py
# Version: 1.0 (2026-08-31)
# License: GPL-3.0-or-later
#
# Sets the admin password on a factory-fresh Reolink camera over its native
# port-9000 protocol, for the case where the web UI's Change Password form
# refuses an empty "old password" and the plain HTTP ModifyUser command answers
# "param error". Uses reolink_aio (MIT, the Home Assistant library) to send the
# same user-list update message that Neolink's "users" command sends
# (Baichuan cmd 59, UserList/User/userSetState=modify).
#
# Usage:
#   python reolink-setpassword.py <camera-ip> --new <new-password>
#   python reolink-setpassword.py <camera-ip> --old <current> --new <new-password>
#
# Password rules on current firmware: 6 to 32 characters, at least two character
# classes (upper, lower, digit, symbol). For Home Assistant keep symbols to
# @ $ * ~ _ - + = ! ? . , : ; ' ( ) [ ]

import argparse
import asyncio
import xml.etree.ElementTree as ET
from xml.sax.saxutils import escape

from reolink_aio.api import Host
from reolink_aio.baichuan import xmls

CMD_GET_USERS = 58
CMD_SET_USERS = 59


def build_user_list(current_xml: str, target: str, new_password: str) -> str:
    root = ET.fromstring(current_xml)
    users = []
    found = False
    for u in root.findall(".//User"):
        name = u.findtext("userName", "")
        level = u.findtext("userLevel", "1")
        uid = u.findtext("userId")
        fields = [f"<userName>{escape(name)}</userName>"]
        if name == target:
            found = True
            fields.append(f"<password>{escape(new_password)}</password>")
        if uid is not None:
            fields.append(f"<userId>{uid}</userId>")
        fields.append(f"<userLevel>{level}</userLevel>")
        if (ls := u.findtext("loginState")) is not None:
            fields.append(f"<loginState>{ls}</loginState>")
        fields.append(f"<userSetState>{'modify' if name == target else 'none'}</userSetState>")
        users.append("<User>" + "".join(fields) + "</User>")
    if not found:
        raise SystemExit(f"user '{target}' not present on camera; users seen: {[u.findtext('userName') for u in root.findall('.//User')]}")
    return xmls.XML_HEADER + '<body><UserList version="1.1">' + "".join(users) + "</UserList></body>"


async def main(ip: str, old: str, new: str) -> None:
    h = Host(ip, "admin", old)
    try:
        current = await h.baichuan.send(cmd_id=CMD_GET_USERS, extension=xmls.UserList.format(username="admin"))
        names = [u.findtext("userName") for u in ET.fromstring(current).findall(".//User")]
        print("users on camera:", names)
        await h.baichuan.send(cmd_id=CMD_SET_USERS, body=build_user_list(current, "admin", new))
        print("password update accepted by camera (status 200)")
    finally:
        await h.logout()

    # verify: a fresh login with the new password over the same protocol
    v = Host(ip, "admin", new)
    try:
        await v.baichuan.get_ports()
        print("verify: login with new password OK")
    finally:
        await v.logout()


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description="Set the admin password on a Reolink camera over port 9000")
    ap.add_argument("ip", help="camera IP address")
    ap.add_argument("--old", default="", help="current admin password (omit for a factory-fresh camera)")
    ap.add_argument("--new", required=True, help="new admin password")
    a = ap.parse_args()
    if not 6 <= len(a.new) <= 32:
        ap.error("new password must be 6 to 32 characters")
    classes = sum(bool(s) for s in (
        any(c.islower() for c in a.new), any(c.isupper() for c in a.new),
        any(c.isdigit() for c in a.new), any(not c.isalnum() for c in a.new)))
    if classes < 2:
        ap.error("new password needs at least two character classes (upper, lower, digit, symbol)")
    asyncio.run(main(a.ip, a.old, a.new))

Windows:

& "$w\Scripts\python.exe" "$w\reolink-setpassword.py" $ip --new 'YourNewPassword1'

Linux:

~/reolink-init/bin/python ~/reolink-init/reolink-setpassword.py $ip --new 'YourNewPassword1'

Single quotes keep the shell from interpreting anything inside the password. Expected:

users on camera: ['admin']
password update accepted by camera (status 200)
verify: login with new password OK

Reload the camera’s web page and log in with the new password. The empty one no longer works. From this point the camera is no longer open to anyone on the network.

Step 6: finish on the camera’s web page

  1. Click the gear icon, then Network, then Advanced, then Port Settings. Make sure the web option is on. Turn on RTSP and ONVIF if your recorder uses them. Save.
  2. Gear icon, then Network, then Wi-Fi. Enter your Wi-Fi name and password, save, and unplug the network cable. The camera switches to Wi-Fi by itself. In your router, reserve the new address so it does not change. Leave the camera’s own setting on DHCP; setting a fixed address inside Reolink’s firmware is known to misbehave.
  3. Add the camera to Home Assistant using the password you set in step 5.
  4. In your router or firewall, block the camera from reaching the internet. Nothing Home Assistant needs goes outside your network.

Cleaning up

Windows: Remove-Item -Recurse -Force "$env:USERPROFILE\Documents\reolink-init". Linux: rm -rf ~/reolink-init. That removes both scripts, the library, and everything else the steps above added.

Which other Reolink cameras this probably applies to

I tested one doorbell. The reasoning below is about what else shares the same machinery; the confidence figures are estimates, not test results.

Cameras Ships locked? Will the fix work? Confidence
Video Doorbell PoE (black and white) Very likely; same firmware family as the WiFi doorbell Yes 90%
RLC series wired cameras (RLC-510A, 810A, 811A, 820A, 823A, 833A, 1212A, 1224A and similar), CX series, Duo 2 and Duo 3, TrackMix, Floodlight, Elite Reports of locked ports on these lines go back to 2022 (RLC) and 2024 (Duo 2) Yes; same private protocol, same port switches 85%
E1 Pro, E1 Zoom, E1 Outdoor, Lumus Pro Likely, on current firmware Yes, where the model has a web server at all; some older hardware revisions of E1 and Lumus have no web, RTSP or ONVIF service to switch on 70%
Reolink NVRs (RLN8, RLN16, RLN36, NVS series) Possibly Untested. The NVR speaks the same protocol, so the switches should be there 60%
Battery cameras: Argus, Altas, Battery Doorbell, Video Doorbell 2nd Gen Not applicable No. These have no web, RTSP or ONVIF service at all; Reolink only exposes ONVIF for them through its Home Hub n/a
B400, B500, B800, B1200, D400, D500, D800, D1200 (kit cameras), original E1 Not applicable No. No web server; they only work through a Reolink NVR or Hub n/a
Go series and TrackMix LTE (cellular) Not applicable No. Different network path; the Home Assistant integration does not support them either n/a

If your model is in the first three rows, run step 1. If 9000 is open and the rest are closed, you are in the same situation and the same fix should apply. The password message in step 5 is the one Neolink has used across the RLC, Duo and E1 lines for years, so it carries the same confidence as the port switch.

What I could not verify

  • Exactly which firmware version first shipped with the ports closed, and whether every unit of every model does. Treat every current Reolink as affected until you check yours.
  • Whether every model accepts the empty password over port 9000 the way this doorbell did, and whether every model applies the user-list update (command 59) the same way. Both are verified on this doorbell only.
  • Reolink’s intent. The locked doors may be a deliberate security choice, and the missing browser setup may be an oversight. Either way the buyer ends up in the same place.

Technical appendix

For readers who want the underlying mechanism, and for the details the plain-language sections leave out.

The protocol. TCP 9000 carries Reolink’s “Baichuan” protocol: obfuscated XML commands in a custom framing, reverse-engineered by the Neolink project and implemented cleanly in reolink_aio. The port-enable command is Baichuan cmd_id 36, with a body of <HttpPort version="1.1"><enable>1</enable></HttpPort> (or HttpsPort, RtspPort, OnvifPort, RtmpPort). reolink_aio.baichuan.set_port_enabled() builds exactly that.

What Home Assistant does on its own. In reolink_aio/api.py, _login_open_port() runs when an HTTP login fails: it calls baichuan.get_ports() over 9000, checks privacy mode, logs the warning “HTTP(s) login failed while Baichuan login succeeded, re-opening HTTP(s) port and looking up correct port on host”, enables HTTPS (or HTTP if the entry was configured for it), sleeps five seconds for the web server to start, and retries. This is the recovery path for a camera whose port was closed by a firmware update. It does not run for a new camera because the integration’s config flow requires a non-empty password before it ever instantiates the library.

Why the empty password works. A factory-fresh Reolink is admin with no password; Reolink’s own documentation says the default password is blank and you are prompted to create one at first login. The Baichuan login accepts that state, which is how the vendor app performs initialization. The script uses it for the one command needed to open the web port, then hands the rest to the browser, where the standard initialization page sets the password.

Why the web page’s own password form fails, and why the plain API fails too. On this firmware the browser client speaks Reolink’s encrypted HTTP mode: every request goes to /cgi-bin/api.cgi?token=...&encrypt=<base64> with a ciphertext body, negotiated at login. The form’s Old Password field is validated client-side as non-empty, so a first password cannot be set through it. The plaintext API path, POST /cgi-bin/api.cgi?cmd=ModifyUser&token=... with {"User":{"userName":"admin","password":...}}, logs in fine with the empty password but answers code 1, rspCode -4, detail "param error" to ModifyUser, with or without oldPassword and level. Whether the camera requires the encrypted channel for user commands or simply a body shape nobody outside Reolink has documented, the effect is the same: no published plaintext form of that command works on this firmware. reolink_aio does not implement the encrypted HTTP mode.

Setting the password over Baichuan. Neolink’s users subcommand documents the message: read the user list with cmd_id 58 (extension <Extension version="1.1"><userName>admin</userName></Extension>), then send cmd_id 59 with a body of <UserList version="1.1"> containing every user, each carrying userName, userId, userLevel, loginState as read, plus <password> and <userSetState>modify</userSetState> on the one being changed and <userSetState>none</userSetState> on the rest. reolink_aio already uses cmd 58 for its own GetUser; the script sends 59 through the library’s generic send(). A 200 status is the camera accepting the list; the script then opens a second session with the new password as proof. Verified on the doorbell: the update was accepted and the empty password stopped working immediately.

The exposure window. Until the password is set, any host that can reach TCP 9000 can issue Baichuan commands as admin. That includes port changes, Wi-Fi credentials, and, as far as the protocol is understood, most of what the app can do. Keep the camera on an isolated VLAN or a dumb switch with one other host during initialization. The same window exists when using the vendor app; the app simply hides it.

Verifying the library before trusting it. reolink_aio 0.21.14: 35 files, 1.6 MB, one non-.py file, MIT. Dependencies: aiohttp, aiortsp, orjson, pycryptodomex, typing_extensions. A grep for https?:// across the source finds ONVIF and OASIS XML namespace identifiers (string constants, not connections), the GitHub repo URL, and four reolink.com endpoints used only by the firmware-update check functions, which this script never calls. The library therefore opens exactly one network destination when run as shown: the camera.

Documentation contradiction, verbatim locations. Reolink’s “Access via Web Browsers” article opens by stating that web access requires the HTTP/HTTPS ports to be open and that you should log in on the Reolink App or Client to open them, and then documents browser login as though it were a first-class path. Reolink’s “Can WiFi cameras work without WiFi or Internet” article says to use an Ethernet cable for first-time setup of a Wi-Fi camera. The Home Assistant integration page, under “Initial setup” > “Connecting Reolink via a web browser”, documents the cable-then-browser path in four steps and adds a self-made QR-code method for Wi-Fi provisioning. The QR method provisions Wi-Fi only; it does not open the web port.

Ports Home Assistant needs across a VLAN boundary. TCP 80 or 443 (API), 554 (RTSP), 1935 (RTMP, needed on some models for the HTTP API to function), 8000 (ONVIF events), 9000 (Baichuan push events). Reolink cameras support a limited number of simultaneous connections; running Home Assistant and an NVR such as Frigate from the same host IP is a documented cause of dropped connections, and FLV is the least demanding stream protocol on the camera side.

IP changes after switching to Wi-Fi. The Home Assistant config entry is keyed on the camera’s MAC. If the Wi-Fi interface reports the same MAC as the wired one, DHCP discovery rewrites the host automatically; if it reports a different MAC, reconfigure and reauth both abort on the mismatch and the entry must be deleted and re-added. Set the Wi-Fi up and reserve the lease before the final Home Assistant add, and the question never arises.

ONVIF and RTSP after the fix. Both stay disabled until enabled in Port Settings or with --ports rtsp,onvif. Reolink is an ONVIF member and lists some products as conformant; whether this doorbell’s firmware is on the conformant list was not verified. In practice its ONVIF has been reported working with Synology Surveillance Station, Frigate (which recommends the HTTP-FLV stream over RTSP for Reolink), and Blue Iris (RTMP recommended there).

The Neolink alternative. The Rust reimplementation of Baichuan carries the same switch as a CLI: neolink services --config <toml> <camera> http on. It needs a TOML config with the camera credentials and either a Rust toolchain or its Docker image. The Python route above is smaller.

Removal is complete. A Python venv is a self-contained directory tree; nothing is written to the system Python, the registry, PATH, or services. Deleting the directory reverts the machine to its prior state.

Sources

3 Likes