Setting up Tor hidden service

Michał ‘mina86’ Nazarewicz | 17 lutego 2019

Anyone can think of myriad reasons to run a Tor hidden service. Surely many unsavoury endeavours spring to mind but of course, there are as many noble ones. There are also various pragmatic causes like circumventing lousy NATs. Me? I just wanted to play around with my router.

Configuring a hidden service is actually straightforward so to make things more interesting, this article will cover configuring a hidden service on a Turris Omnia router with the help of Linux Containers to maximise isolation of components. While some steps will be Omnia-specific, most translate easily to other systems, so this post may be applicable regardless of the distribution used.

External drive

Turris Omnia uses an eMMC as its root device which, on the count of it being soldered onto the board, is hard to replace. To mitigate the risk of it wearing off, data can be saved onto external storage instead. The router comes with mSATA slot and USB ports. Either can be used to attach a drive. In addition, mPCIE SATA controller is included in the NAS version of the router and can be added to regular versions.

No matter how additional storage is attached, it can be mounted under /srv directory which is where many applications will store their data. This is a completely adequate solution but there’s also a more… exciting alternative: making the router boot from the external drive. The upside is that the eMMC will never wear off. The downside is that it won’t work with a storage device attached through a SATA controller.

First, access to the router’s serial console needs to be established. Any USB to UART connector—such as TTL-232R-RPI or something PL2303-based—should work. If pinout of the converter isn’t documented, it’s usually enough to open it and look at the labels printed on its PCB. On Omnia’s side, UART header is located between LEDs and brightness button. Starting from the side close to the LEDs, the pins are ground, RX, TX and usually unused +3.3V.

Once the connection to the serial console is established, configuring the router to boot from an external drive is as easy as grabbing Omnia’s medkit and following official instructions. One thing to consider is that /dev/sda1 name is unstable so rather than root=/dev/sda1 kernel argument advocated by the official method it’s better to use partition UUID. For MBR partition tables, it is composed of disk identifier and partition number which can both be obtained from the output of fdisk -l.

# fdisk -l /dev/sda
Disk /dev/sda: 55.9 GiB, 60022480896 bytes, 117231408 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xe17eb372

Device     Boot Start       End   Sectors  Size Id Type
/dev/sda1        2048 117231407 117229360 55.9G 83 Linux

For example, if the disk identifier is e17eb372 (as shown in listing above), root=​PARTUUID=e17eb372-01 should be used instead of root=/dev/sda1 when setting bootloader’s bootargs variable. If the time ever comes to replace the disk, the identifier of a new drive can be set to match the original using fdisk’s expert mode.

Linux Containers

To improve isolation between components, it’s a good idea to take advantage of Linux Containers (LXC). On Omnia, LXC utilities are installed in the Updater section of Foris interface. The installation procedure will differ on other systems and their documentation should be consulted.

Web server

Local network192.168.1.0/24
Gateway & DNS192.168.1.1
Web server host192.168.1.80

This article will use a web server as an example of hidden service’s back-end, but one should remember that anything accepting TCP connections can be used: SSH server, IRC bouncer and SOCKS proxy are all valid candidates.

First, a new container is needed. Because alpine is a lightweight, security-oriented Linux distribution it is a good choice for container’s base image. To make things easier, it will be configured with static IP of 192.168.1.80 with 192.168.1.1 default gateway and DNS server.

[turris]# lxc-create -n www -P /srv/lxc -t download -- \
                     -d Alpine -r Edge -a "$(uname -m)"
[turris]# cat >/srv/lxc/www/rootfs/etc/network/interfaces \
              <<EOF
auto eth0
iface eth0 inet static
        address 192.168.1.80
        netmask 255.255.255.0
        gateway 192.168.1.1
hostname \$(hostname)
EOF
[turris]# echo nameserver 192.168.1.1 \
               >/srv/lxc/www/rootfs/etc/resolv.conf
[turris]# lxc-start -n www
[turris]# lxc-attach -n www
[www]# apk add lighttpd logrotate
[www]# rc-update add lighttpd
[www]# rc-service lighttpd start
[www]# exit

At this point, 192.168.1.80 should lead to ‘It works’ web page. Since the HTTP server does not need to make any outgoing connections, the image can (should) have network traffic restricted with the following rules:

[turris]# opkg install iptables-mod-extra
[turris]# lxc-attach -n www
[www]# apk add iptables
# Allow incoming connections from LAN only
[www]# iptables -A INPUT -s 192.168.1.0/16 -j ACCEPT
[www]# iptables -A INPUT -m state --state NEW -j DROP
# Don’t allow any forwarding
[www]# iptables -P FORWARD DROP
# Let root resolve domain names
[www]# iptables -A OUTPUT -d 192.168.1.1 -p udp --dport 53 \
                          -m owner --uid-owner 0 -j ACCEPT
[www]# iptables -A OUTPUT -d 192.168.1.1 -p tcp --dport 53 \
                          -m owner --uid-owner 0 -j ACCEPT
# Don’t allow initiating connections to LAN
[www]# iptables -A OUTPUT -d 192.168.1.0/16 -m state --state NEW -j DROP
[www]# iptables -A OUTPUT -d 192.168.1.0/16 -j ACCEPT
# Allow only root to talk to the Internet
[www]# iptables -A OUTPUT -m owner --uid-owner 0 -j ACCEPT
[www]# iptables -P OUTPUT DROP
# Store and apply the rules on each boot
[www]# rc-update add iptables
[www]# /etc/init.d/iptables save
[www]# exit

Tor

The next step is setting up Tor. Like before, it’s going to run inside of a container to maximise isolation. However, this time the image will be using dynamic IP since its address won’t be hard-coded anywhere.

[turris]# lxc-create -n tor -P /srv/lxc -t download -- \
                     -d Alpine -r Edge -a "$(uname -m)"
[turris]# lxc-start -n tor
[turris]# lxc-attach -n tor
[tor]# apk add tor logrotate
[tor]# cat >/etc/tor/torrc <<EOF
User tor

SOCKSPort 0
SOCKSPolicy reject *
ExitRelay 0
ExitPolicy reject *:*

Log notice file /var/log/tor/notices.log

DataDirectory /var/lib/tor
HiddenServiceDir /var/lib/tor/hidden
HiddenServiceVersion 3
HiddenServicePort 80 192.168.1.80:80
EOF
[tor]# rc-update add tor
[tor]# rc-service tor start
[tor]# exit

Once Tor is started, it’ll automatically generate keys for the hidden service and save its .onion address in /var/lib/tor/hidden-site/hostname file (or /srv/lxc/tor/rootfs/var/lib/tor/hidden-site/hostname if acessing it from outside of the container).

At this point, everything should work. The web server should be accessible via its local address as well as through its .onion address.

To finish up with the container, firewall rules need to be created. Tor needs to be able to talk to the Internet and to the web server but does not need to connect to any other hosts on the local network nor accept any incoming connections. This can be codified with the following instructions:

[turris]# opkg install iptables-mod-extra
[turris]# lxc-attach -n tor
[tor]# apk add iptables
# Disallow incoming connections
[tor]# iptables -A INPUT -m state --state NEW -j DROP
# Don’t allow any forwarding
[tor]# iptables -P FORWARD DROP
# Allow talking to local DNS
[tor]# iptables -A OUTPUT -d 192.168.1.1 -p udp --dport 53 -j ACCEPT
[tor]# iptables -A OUTPUT -d 192.168.1.1 -p tcp --dport 53 -j ACCEPT
# and the web server that’s being hidden
# iptables -A OUTPUT -d 192.168.1.80 -p tcp --dport 80 -j ACCEPT
# but no other host in LAN.
# iptables -A OUTPUT -d 192.168.1.0/16 -j DROP
# Store and apply the rules on each boot
[tor]# rc-update add iptables
[tor]# /etc/init.d/iptables save
[tor]# exit

Making containers start at boot

The final thing to do is make both containers start when the router boots. Otherwise, the hidden service will stop working as soon as host reboots. In Omnia this is done by editing /etc/config/lxc-auto file to contain the following:

config container
	option name www

config container
	option name tor

Security considerations

While setting up a hidden service is trivial, making it secure is another matter. It’s not inconceivable that some servers may be tricked to leak information about their external IP. Perhaps an FTP server is made to make an active data connection over the Internet. Maybe an HTTP server displays its external address in error pages. Not to mention arbitrary command execution exploits which could be used to make simple requests over the Internet. Restricting service’s back-end access to the Internet (as has been done in this article) or configuring it to only ever use Tor circuits is one defence.

It’s also important to keep in mind that Tor relays report all their bandwidth publicly. In other words, if a process providing a hidden service is also running as a relay, it is theoretically possible to locate the service by issuing requests to it and observing transfer reported by the relay. As such, a Tor relay shouldn’t be run on an instance which is also providing a hidden service.

Security of the service is beyond the scope of this article (especially as in my case privacy is not of paramount importance) so the reader is encouraged to do their own due diligence.