Making the Valetuduo UI available from outside

shimun December 30, 2025 #nixos #valetudo #nginx #wstunnel

Preface

Valetudo is an great replacement for the OEMs cloud native firmware. The cloud however, still has some advantages the biggest being imo, to be able to start cleaning when everyone has left the home. Starting the cleaning job while still in wifi range can be challening when in a hurry.

Possible solutions

In order to make Valetudo accessible from outside my wifi I've explored a couple options:

Wireguard

Wireguard seemed obvious since I already operate an network with all my personal device in it so adding the robot seemed natural. While the kernel on the robot does not come with wireguard support, the lack thereof can easily be compensated for by compiling either boring-tun or wireguard-go statically for aarch64. An issue arises however when trying to configure the interface due to an lack of the required depenencies for wg-quick. Surely those issues can be worked out but would take some effort and result in a rather fragile solution.

WsTunnel

overview

WsTunnel makes things a lot simpler on the robots side since WsTunnel is available as an statically compiled binary for aarch64 with no other depenencies in form of setup scripts, so lets start there:

Download WsTunnel for aarch64 from Github releases extract the tgz and then copy the binary to the robot

cat wstunnel | ssh root@<robot-ip> "cat > /data/wstunnel; chmod +x /data/wstunnel"

Confirm the binary is indeed runable on our robot:

# /data/wstunnel -h
Use Websocket or HTTP2 protocol to tunnel {TCP,UDP} traffic
wsTunnelClient <---> wsTunnelServer <---> RemoteHost

Usage: wstunnel [OPTIONS] <COMMAND>

Commands:
  client
  server
  help    Print this message or the help of the given subcommand(s)

Options:
      --no-color <NO_COLOR>      Disable color output in logs [env: NO_COLOR=]
      --nb-worker-threads <INT>  *WARNING* The flag does nothing, you need to set the env variable *WARNING*
                                 Control the number of threads that will be used.
                                 By default, it is equal the number of cpus [env: TOKIO_WORKER_THREADS=]
      --log-lvl <LOG_LEVEL>      Control the log verbosity. i.e: TRACE, DEBUG, INFO, WARN, ERROR, OFF
                                 for more details: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax [env: RUST_LOG=] [default: INFO]
  -h, --help                     Print help
  -V, --version                  Print version

Success

Now that we know that we won't run in any trouble on the robots side we can move on to setting up the server. In order to allow reverse tunneling we have to provide WsTunnel with an config since the CLI cannot express reverse tunneling options.

restrict.yml:

restrictions:
  - name: "Reverse"
    match:
     - !Any
    allow:
      - !ReverseTunnel
        protocol:
          - Tcp
          - Udp
        port:
          - 7732

With this config in place the tunnel can be started:

wstunnel \
  server \
  '--restrict-config restrict.yml' \
  ws://127.0.0.1:7733

The process needs to be daemonized somehow, I've opted to use the services.wstunnel NixOS module to generate an systemd-unit but for other distros that unit can be easily written by hand.

WsTunnel will now accept connections on 127.0.0.1:7733 which is quite useless since the robot cannot reach localhost on the server, thats why I've setup nginx such that it proxies requests from an virtual host to the local WsTunnel socket. I've also configured another virtual host to serve the tunneled Valetudo UI to clients in my wireguard network.

Now the only thing left to do is to terminate the tunnel on the robot:

/data/wstunnel client --remote-to-local tcp://7732:127.0.0.1:80 wss://<nginx websocket endpoint>

Once started the UI should be available via nginx.

The final touch

The tunnel will terminate as soon as out ssh session ends which makes the whole exerise quite pointless, therefore the process needs to be daemonized. Which is easier said than done since the rootfs on my robot is a readonly sqashfs which means there is no way to add to /etc/init.d. ButI've figured there is a script which starts Valetudo and said script resides on /data which is writeable so I've added:

if [[ -f /data/tunnel.sh ]]; then
        /data/tunnel.sh > /dev/null 2>&1 &
fi

to /data/_root_postboot.sh with tunnel.sh containing the WsTunnel command.

Time to reboot and test