Skip to content

Commit f4aca8d

Browse files
authored
Add basic reverse proxy support (#50)
1 parent c243c92 commit f4aca8d

14 files changed

Lines changed: 243 additions & 9 deletions

File tree

config.example.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ bind_host = "0.0.0.0"
55
# Change these if you need custom published ports.
66
https_port = 555
77
mqtt_tls_port = 8881
8+
# Optional reverse-proxy support. Leave unset to advertise the listener ports above.
9+
# Set these when a proxy maps public HTTPS/MQTT ports to different backend ports.
10+
# advertised_https_port = 443
11+
# advertised_mqtt_tls_port = 8883
812
region = "us"
913

1014
[broker]

docs/home_assistant.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ If you need the MITM protocol sync secret for the Roborock app flow, sign in to
6464

6565
- The add-on always runs the embedded MQTT broker and keeps the topic bridge enabled.
6666
- The add-on terminates TLS itself and publishes two ports: HTTPS on `https_port` and MQTT/TLS on `mqtt_tls_port`.
67-
- If you already manage certificates in another Home Assistant add-on such as Nginx Proxy Manager, you can point `cert_file` and `key_file` at those PEM files through `/all_addon_configs/...`.
67+
- If you already manage certificates in another Home Assistant add-on such as Nginx Proxy Manager, you can point `cert_file` and `key_file` at those PEM files through `/all_addon_configs/...`. Nginx Proxy Manager is mainly useful here as a certificate source or admin/API HTTPS convenience; it does not remove the need for a reachable MQTT/TLS port. See [Reverse Proxy](reverse_proxy.md).
6868
- Installing the add-on does **not** automatically rewrite Home Assistant's Roborock integration entry.
6969

7070
## Repoint The Home Assistant Roborock Integration

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ If you want to support this project, next time you buy a Roborock, use one of my
2323
- [Using the Roborock App](roborock_app.md)
2424
- [Updating](updating.md)
2525
- [Tested vacuums](tested_vacuums.md)
26+
- [Reverse proxy](reverse_proxy.md)
2627
- [Custom MQTT](custom_mqtt.md)
2728
- [Custom certificate management](custom_cert_management.md)
2829
- [Technical Writeup](technical_writeup.md)

docs/installation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ If your model already has certificate notes on the tested-vacuums page, follow t
6666

6767
With the current server behavior, the same hostname is advertised for both HTTPS and MQTT/TLS, so you do not need a separate `mqtt-...` hostname unless you have built your own custom client routing around one.
6868

69+
If a reverse proxy maps public ports to different backend listener ports, see [Reverse Proxy](reverse_proxy.md) before starting the stack.
70+
6971
## Method 1: Docker Compose
7072

7173
### Additional Requirements
@@ -134,6 +136,8 @@ If your model already has certificate notes on the tested-vacuums page, follow t
134136
docker compose up -d --build
135137
```
136138

139+
For reverse proxy setups, keep `network.https_port` and `network.mqtt_tls_port` set to the backend listener ports and use `network.advertised_https_port` / `network.advertised_mqtt_tls_port` for the public ports.
140+
137141
## Method 2: Home Assistant Add-on
138142

139143
Use [Home Assistant](home_assistant.md) as the installation guide if you want to run the stack as a Home Assistant add-on instead of Docker Compose.

docs/reverse_proxy.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Reverse Proxy
2+
3+
Reverse proxy support is mainly useful when public or LAN clients reach the stack on different ports than the backend listeners. The server still runs its own TLS listeners for both HTTPS and MQTT/TLS; the proxy forwards traffic to those listeners. I do not use a reverse proxy for my own setup, so please report any issues.
4+
5+
## Supported Layout
6+
7+
Use this when:
8+
9+
- HTTPS reaches the proxy on `443`, then forwards to the stack HTTPS listener such as `555`
10+
- MQTT/TLS reaches a TCP/stream proxy on `8883`, then forwards to the stack MQTT/TLS listener such as `8881`
11+
- the proxy preserves the original `Host` header
12+
13+
Example:
14+
15+
```toml
16+
[network]
17+
stack_fqdn = "api-roborock.example.com"
18+
bind_host = "0.0.0.0"
19+
20+
# Backend listener ports.
21+
https_port = 555
22+
mqtt_tls_port = 8881
23+
24+
# Public ports advertised to the Roborock app, vacuums, and Home Assistant.
25+
advertised_https_port = 443
26+
advertised_mqtt_tls_port = 8883
27+
```
28+
29+
With that config the server listens on `https://*:555` and `ssl://*:8881`, but responses advertise:
30+
31+
- `https://api-roborock.example.com`
32+
- `ssl://api-roborock.example.com:8883`
33+
34+
## Proxy Requirements
35+
36+
For HTTPS admin/API traffic, the proxy must forward the original `Host` header unchanged:
37+
38+
```text
39+
Host: $host
40+
```
41+
42+
For MQTT/TLS, use TCP or stream proxying. A normal HTTP reverse proxy location is not enough because MQTT is not HTTP. The proxy must forward raw TCP from the public MQTT/TLS port to `mqtt_tls_port`.
43+
44+
## What Is Not Supported
45+
46+
Path-prefix hosting is not supported. The Roborock protocol and the admin API expect the stack at the hostname root, for example `/region`, `/api/...`, and `/admin`.
47+
48+
Plain HTTP backends are not supported. If you already manage certificates in a proxy, point `tls.cert_file` and `tls.key_file` at those certificate files so the backend TLS listener uses the same certificate chain.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ nav:
5959
- Reference:
6060
- Known Limitations: known_limitations.md
6161
- Tested Vacuums: tested_vacuums.md
62+
- Reverse Proxy: reverse_proxy.md
6263
- Custom MQTT: custom_mqtt.md
6364
- Custom Certificate Management: custom_cert_management.md
6465
- Technical Writeup: technical_writeup.md

roborock_local_server_addon/DOCS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ Use **Reconfigure** on the Roborock integration after Home Assistant has loaded
4040
- If you change `https_port` or `mqtt_tls_port`, update your DNS/clients to use those ports.
4141
- The current server advertises the same hostname for HTTPS and MQTT/TLS, so Home Assistant's Roborock entry should normally use `ssl://api-roborock.example.com:8881`, not a separate `mqtt-...` hostname.
4242
- If you already manage certificates in another Home Assistant add-on such as Nginx Proxy Manager, you can point `cert_file` and `key_file` at that add-on's certs through `/all_addon_configs/...`. Example: `/all_addon_configs/a0d7b954_nginxproxymanager/letsencrypt/live/npm-3/fullchain.pem`.
43+
- If a reverse proxy exposes different public ports than the add-on listeners, keep `https_port`/`mqtt_tls_port` as the add-on listener ports and set `advertised_https_port`/`advertised_mqtt_tls_port` to the public ports. The proxy must preserve the original `Host` header, and MQTT/TLS still needs a reachable port or a TCP/stream proxy.

roborock_local_server_addon/config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ options:
2525
stack_fqdn: "api-roborock.example.com"
2626
https_port: 555
2727
mqtt_tls_port: 8881
28+
advertised_https_port: 0
29+
advertised_mqtt_tls_port: 0
2830
region: "us"
2931
tls_mode: "provided"
3032
tls_base_domain: ""
@@ -42,6 +44,8 @@ schema:
4244
stack_fqdn: str
4345
https_port: port
4446
mqtt_tls_port: port
47+
advertised_https_port: int
48+
advertised_mqtt_tls_port: int
4549
region: list(us|eu|cn|ru)
4650
tls_mode: list(provided|cloudflare_acme)
4751
tls_base_domain: str

src/roborock_local_server/config.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class NetworkConfig:
1515
bind_host: str
1616
https_port: int
1717
mqtt_tls_port: int
18+
advertised_https_port: int
19+
advertised_mqtt_tls_port: int
1820
region: str
1921
localkey: str
2022
duid: str
@@ -201,12 +203,25 @@ def load_config(path: str | Path) -> AppConfig:
201203
broker_host = "127.0.0.1"
202204
broker_port_default = 18830 if broker_mode == "embedded" else 1883
203205

206+
https_port = _as_port(network.get("https_port"), "network.https_port", 555)
207+
mqtt_tls_port = _as_port(network.get("mqtt_tls_port"), "network.mqtt_tls_port", 8881)
208+
204209
config = AppConfig(
205210
network=NetworkConfig(
206211
stack_fqdn=_require_stack_fqdn(network.get("stack_fqdn"), "network.stack_fqdn"),
207212
bind_host=str(network.get("bind_host", "0.0.0.0")).strip() or "0.0.0.0",
208-
https_port=_as_port(network.get("https_port"), "network.https_port", 555),
209-
mqtt_tls_port=_as_port(network.get("mqtt_tls_port"), "network.mqtt_tls_port", 8881),
213+
https_port=https_port,
214+
mqtt_tls_port=mqtt_tls_port,
215+
advertised_https_port=_as_port(
216+
network.get("advertised_https_port"),
217+
"network.advertised_https_port",
218+
https_port,
219+
),
220+
advertised_mqtt_tls_port=_as_port(
221+
network.get("advertised_mqtt_tls_port"),
222+
"network.advertised_mqtt_tls_port",
223+
mqtt_tls_port,
224+
),
210225
region=str(network.get("region", "us")).strip().lower() or "us",
211226
localkey=str(network.get("localkey", "")).strip(),
212227
duid=str(network.get("duid", "")).strip(),

src/roborock_local_server/ha_addon.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"stack_fqdn": "",
1818
"https_port": 555,
1919
"mqtt_tls_port": 8881,
20+
"advertised_https_port": 0,
21+
"advertised_mqtt_tls_port": 0,
2022
"region": "us",
2123
"tls_mode": "provided",
2224
"tls_base_domain": "",
@@ -85,6 +87,12 @@ def _as_int(value: object, *, field_name: str, default: int) -> int:
8587
return candidate
8688

8789

90+
def _as_optional_port(value: object, *, field_name: str) -> int:
91+
if value in (None, "", 0, "0"):
92+
return 0
93+
return _as_int(value, field_name=field_name, default=0)
94+
95+
8896
def _require_non_empty(value: object, *, field_name: str) -> str:
8997
text = str(value or "").strip()
9098
if not text:
@@ -158,6 +166,16 @@ def _render_config_toml(
158166
raise ValueError("listener_mode='external_tls' is no longer supported")
159167
https_port = _as_int(merged.get("https_port"), field_name="https_port", default=555)
160168
mqtt_tls_port = _as_int(merged.get("mqtt_tls_port"), field_name="mqtt_tls_port", default=8881)
169+
advertised_https_port = _as_optional_port(
170+
merged.get("advertised_https_port"),
171+
field_name="advertised_https_port",
172+
)
173+
advertised_mqtt_tls_port = _as_optional_port(
174+
merged.get("advertised_mqtt_tls_port"),
175+
field_name="advertised_mqtt_tls_port",
176+
)
177+
advertised_https_port = advertised_https_port or https_port
178+
advertised_mqtt_tls_port = advertised_mqtt_tls_port or mqtt_tls_port
161179

162180
# Legacy HA options for broker selection are ignored now that the add-on
163181
# always runs the embedded broker with the topic bridge enabled.
@@ -218,6 +236,8 @@ def _render_config_toml(
218236
'bind_host = "0.0.0.0"',
219237
f"https_port = {https_port}",
220238
f"mqtt_tls_port = {mqtt_tls_port}",
239+
f"advertised_https_port = {advertised_https_port}",
240+
f"advertised_mqtt_tls_port = {advertised_mqtt_tls_port}",
221241
f"region = {_toml_string(region)}",
222242
"",
223243
"[broker]",

0 commit comments

Comments
 (0)