Some­time last year I found my­self need­ing to spend all of the fake Amazon money that I keep re­ceiv­ing with every busi­ness trip. With dog­go’s fur shed­ding sea­son right around the corner, a va­cuum ro­bot def­in­itely seemed like an ap­pro­pri­ate way to con­sume at the time.

But wait… there’s a snag! Well built va­cu­ums at a reas­on­able price point all hap­pen to be China Ex­port vari­ety of In­ter­net of Trash. I would never put any­thing of the sort any­where near my LAN in the first place, but even if it did some­how man­age to get on there, the device would find it­self quite alone and isol­ated in its very own VLAN. What good is a va­cuum ro­bot without a way to re­quest it to en­sure a ba­sic level of hy­giene in Room D?

Turns out some­body else had much the same con­cerns well be­fore me and came up with Va­le­tudo. A couple broken plastic tabs off the plastic cover later – elec­tron­ics’ dis­as­sembly is not my strong suit – I’m a proud owner of a ssh ses­sion to a reas­on­ably stand­ard look­ing Linux en­vir­on­ment on wheels. It does­n’t take long for pe­cu­li­ar­it­ies to crop up, though. Neither Tails­cale, nor vanilla wire­guard would agree to run on this device, for in­stance. Both de­pend on at least iptables which is not avail­able in this en­vir­on­ment. A closer look re­veals that this ker­nel was built with CONFIG_NETFILTER=y and even CONFIG_NETFILTER_ADVANCED=y op­tions, but CONFIG_NETFILTER_XTABLES that’s re­quired for iptables did­n’t make the cut for whatever reas­on. Weird, huh? There went all my hopes of con­trolling this won­der­ful new device from across the planet through my usual in­fra­struc­ture.

Then just last month some­body claimed Tails­cale sucks, all while provid­ing a step-by-step set up of Tails­cale with user­space net­work­ing (TIL). On a va­cuum ro­bot. That’s run­ning Va­le­tu­do. Well now, is­n’t this ex­actly the setup I have?! Be­hold an­other tail­net with a va­cuum ro­bot… Or so I wished. This would­n’t be a blog post re­spon­se, if there was­n’t some­thing lack­ing with the setup as de­scribed, would it?

See, fol­low­ing these in­struc­tions does in­deed put a Tails­cale on a va­cuum ro­bot, but the ker­nel on the ro­bot has no idea about any of it. This is not a con­cern when the con­nec­tions are all in­bound. As sug­ges­ted by the blog post, it is pos­sible to ac­cess Va­le­tudo fron­tend via HT­TP, and you can even SSH into the ro­bot from the out­side. But a proper setup of the ro­bot for home auto­ma­tion will use neither of these meth­ods. The com­mu­nic­a­tion method of choice here would be MQTT, which un­like the other two, ex­pects the ro­bot to ini­ti­ate the out­bound con­nec­tion rather than the other way around. Since this Tails­cale setup is en­tirely in the user­-space, any connect(2) and sim­ilar such calls will still use the usual ker­nel’s view of how the net­work is setup; any at­tempts to con­nect to an MQTT broker over the tail­net will fail!

To en­able these sorts of use-cases, Tails­cale is able to set up a SOCKS5 proxy. This likely works al­right for most typ­ical con­tain­er­ized server ap­plic­a­tions, but Va­le­tudo is not in­tern­ally proxy aware. Work­arounds it is, then! proxy­chains comes up as the very first op­tion to try, but as far as I can tell, this won’t work with static bin­ar­ies, which Va­le­tudo (and oth­ers, for that mat­ter) are. By my eval­u­ation an over­all sim­pler solu­tion is a simple bin­ary that would set up a socket listen­ing on loc­al­host, and then for­ward any pack­ets com­ing in over the SOCKS5 proxy ex­posed by Tails­cale. I could then point the Va­le­tu­do’s MQTT cli­ent to this loc­al­host socket and reap all the profit!

I de­scribed this idea in depth on this Tails­cale fea­ture re­quest. I can’t wait for it to get im­ple­men­ted though, my floor are still dirty now! I then re­dis­covered a com­mon­place tool called socat can do al­most ex­actly this. The up­stream socat might not sup­port SOCKS5 at the time of writ­ing, but a kind soul has pro­duced a branch with this func­tion­al­ity in place. All that’s left is to build socat and get it onto the ro­bot! One caveat: I also wanted a re­cent re­lease of socat (for no other reason than it be­ing fresh; you might not need it), so I cloned the up­stream re­pos­it­ory, re­based the SOCKSv5 patches on top and built the thing manu­ally:

git clone 'git://repo.or.cz/socat.git'
cd socat
git remote add socksv5 'https://github.com/runsisi/socat.git'
git fetch socksv5
git checkout socksv5/master
git rebase origin/master
autoconf
env CC=aarch64-unknown-linux-gnu-gcc CFLAGS=-static LDFLAGS=-static \
  ./configure --disable-openssl --disable-readline \
              --disable-system --disable-pty --disable-sctp
make socat -j

One im­port­ant thing to note about the build pro­cess above: just like with tails­cale we want a static bin­ary without de­pend­en­cies. glibc does not sup­port some of the net­work­ing re­lated func­tion­al­ity in such builds and you will see some warn­ings to that ef­fect dur­ing the build pro­cess. However, for this use-case none of the af­fected func­tions are used – socat will work fine re­gard­less. That said, make sure to ob­tain/in­stall the static glibc lib­rar­ies for aarch64 in ad­di­tion to the cross-­com­piler it­self.

I also made a couple of opin­ion­ated choices here. For in­stance I dis­abled sup­port for openssl and readline. These sound like they might de­pend on ex­ternal lib­rar­ies. Ob­tain­ing them is a ma­jor pain and they would make the bin­ary lar­ger for no good reas­on. An even leaner socat might be pos­sible if fur­ther pro­to­cols are dis­abled (see ./configure --help).

Any­way, copy over socat onto the ro­bot, chmod +x it, and ad­just _root_postboot.sh so that it con­tains:

if [[ -f /data/tailscaled ]]; then
    mkdir -p /data/tailscale-state /tmp/tailscale
    STATE_DIRECTORY=/tmp/tailscale /data/tailscaled \
        --tun=userspace-networking \
        --socks5-server=localhost:1055 \
        --socket=/tmp/tailscale/tailscaled.sock \
        --statedir=/data/tailscale-state > /dev/null 2>&1 &
fi
if [[ -f /data/socat ]]; then
    /data/socat TCP-LISTEN:1883,fork \
        SOCKS5:127.0.0.1:[mqtt broker's tail name]:1883,socks5port=1055 &
fi

Here the tailscale com­mand has been mod­i­fied since the ori­ginal blog post to in­tro­duce the --socks5-server=localhost:1055 flag. The last re­main­ing step is to modify the Va­le­tu­do’s MQTT con­fig­ur­a­tion to point at the loc­al­host socket cre­ated by socat and ap­pre­ci­ate Tails­cale’s fu­tile ef­forts in keep­ing the floor free of fur.