Running an OpenBSD Tor proxy in Qubes

Install OpenBSD as HVM Template

Follow the normal process for creating a template that is not cloned from an existing template. In the GUI select the options to Launch Qube settings after creation and Do not clone. In the qube Settings make sure that virt_mode is set to HVM, Kernel is (provided by qube), deselect Include in memory balancing, and set Initial memory to 400. Boot the new template from an OpenBSD iso, and run a standard install on to the first disk. Alternatively, there is a pre-built OpenBSD template available from 3isec

Install and configure tor and pf

I have already covered Running a Tor proxy on OpenBSD so I will summarise here.

pkg_add tor

I also install curl (for testing) and nyx to monitor circuits. pkg_add curl nyx

Configure tor and pf

The configuration file is at /etc/tor/torrc and is well documented, both internally and with man tor. I make the following configuration changes:

SOCKSPort "127.0.0.1:9050 IsolateClientAddr IsolateSocksAuth" # Default: Bind to localhost:9050 for local connections.
TransPort "127.0.0.1:9040 IsolateClientAddr" #
TransProxyType pf-divert
VirtualAddrNetwork "172.16.0.0/12"

DNSPort "127.0.0.1:53 IsolateClientAddr IsolateSocksAuth"
AutoMapHostsOnResolve 1

In the section on entry policies I set:

SOCKSPolicy accept 127.0.0.1
SOCKSPolicy accept 10.0.0.0/8

I also set Control Port 9051.

The tor daemon is listening on localport 127.0.0.1, and provides a SOCKS proxy, a transparent proxy, and Tor DNS resolver.

Configure pf

Incoming SOCKS traffic is redirected to the SOCKS Port, DNS traffic to the DNS Port, and all other TCP traffic is routed to the TRANSPort. Anything else is dropped. OUTGOING traffic is restricted to the tor daemon.

In /etc/pf.conf:

ext_if    = "xnf0"
internal  = "10.0.0.0/8"
SOCKS_IP  = "127.0.0.1"
TRANS_IP  = "127.0.0.1"
DNS_IP    = "127.0.0.1"
SOCKSPort = 9050
TRANSPort = 9040
DNSPort         = 53

block all
set skip on lo0

pass out on $ext_if proto tcp from any user _tor

pass in quick on $ext_if proto tcp \
  from any to any port $SOCKSPort \
    rdr-to $SOCKS_IP port $SOCKSPort

pass in on $ext_if proto udp \
  from $internal to port $DNSPort \
  rdr-to $DNS_IP port $DNSPort

pass in on $ext_if proto tcp \
  from $internal to any \
    divert-to $TRANS_IP port $TRANSPort

Change DNS resolution to use Tor

Edit /etc/resolv.conf to:

nameserver 127.0.0.1

Restart the firewall

pfctl -f /etc/pf.conf

Restart tor

rcctl enable tor
rcctl start tor

You should see tor(ok)

Test the circuit

A simple test is to run: netstat -a -f inet You should see listening ports as specified for the tor daemon.

Once you have the tor daemon running and have confirmed that it is listening properly, you can test it is working:

curl -x socks5h://127.0.0.1:9050 -s https://check.torproject.org |head -n20|grep Tor

This will test both DNS and SOCKS connectivity over Tor. If successful you will see "Congratulations. This browser is configured to use Tor." for each request.

Configure the net qube

Create a new qube - here called bsd_firewall to act as net qube for the OpenBSD Tor qube and clients. You could clone sys-firewall. Make sure you set the qube preferences to include provides_network, and that the upstream qubes do not include one providing Tor services. (You can no longer run Tor over Tor.)

In the bsd_firewall qube, add the following to /rw/config/rc.local:

# SET THE IP OF THE BSD_TOR QUBE HERE
TOR_IP="10.137.0.105"
# SET THE IP OF THE BSD_TOR QUBE HERE

mkdir  /etc/iproute2/
echo "100 to_proxy" >> /etc/iproute2/rt_tables
ip route add default via $TOR_IP table to_proxy

Replace 10.137.0.105 with the IP address of the OpenBSD qube running tor.

What this does is create a new routing table, called to_proxy, when the bsd_firewall starts up. We will use this to override normal Qubes networking routing, so that all traffic from client qubes gets directed to the OpenBSD Tor qube.

You could set nftables to pass all traffic between connected qubes, but lets do something a little more interesting. It's possible to run a script when there is a change in the status of an attached interface: the file is created in /rw/config/network-hooks.d/ : you may need to create that directory. Executable files placed here are run when Qubes interfaces are configured, as documented here

Here is a sample script: change TOR_IP to that of the OpenBSD qube:

#!/bin/bash

# SET THE IP OF THE BSD_TOR QUBE HERE
TOR_IP="10.137.0.105"
# SET THE IP OF THE BSD_TOR QUBE HERE

TOR_VIF=$(ip route |awk -v var="$TOR_IP" '$1 == var { print $3 }')

command="$1"
vif="$2"
vif_type="$3"
ip="$4"

echo "$command  $vif  $vif_type  $ip " >> /rw/config/log
echo "=================== " >> /rw/config/log

table_qubes=$(mktemp)

set_nft() {
  nft list table ip qubes > $table_qubes
  sed -i -e '1i \#\!/usr/sbin/nft -f\nflush table ip qubes\nflush set ip qubes allowed\n' \
         -e "s/10.139.1.[12]/$1/2 " \
         -e "/drop forward:/d " \
         -e "/eth0/d " \
         -e "/chain antispoof/a iifname $2 accept" \
         -e "/chain custom-forward/a iifname $2 oifname eth0 ct state new,established,related counter accept" \
         -e "/chain custom-forward/a iifname eth0 oifname $2 ct state established counter accept" \
         -e "/chain custom-forward/a oifname $2 ip saddr $3 ct state new,established counter accept" \
         -e "/chain custom-forward/a iifname $2 ip daddr $3 ct state established counter accept " \
         -e "/chain custom-forward/a iifname $4 ip daddr $1 ct state established,new counter accept " \
      -e '/chain custom-forward {/,/^[[:space:]]*}/{
        /^[[:space:]]*}/i\
    counter log prefix "drop forward: " drop
          }' \
      $table_qubes

  nft -f $table_qubes
}

clear_nft () {
  echo $1
  echo $2
  rm /tmp/tmp*
  nft list table ip qubes > $table_qubes
  sed -i -E -e '1i \#\!/usr/sbin/nft -f\nflush table ip qubes\nflush set ip qubes allowed\n' \
    -e "/$2/{ /[{}]/!d; s/\"[^\"]*\" \. $2[, ]*//; s/$2,? *//; }" \
    -e "/$3/d" \
      $table_qubes
  nft -f $table_qubes
}


if [ "$ip" == $TOR_IP ]; then
  case "$command" in
    online)
      ip route add default via $TOR_IP table to_proxy
      ;;
    offline)
      ;;
  esac
else
  case "$command" in
    online)
      ip route add default via $TOR_IP table to_proxy
      ip rule add from $ip lookup to_proxy
      set_nft "$TOR_IP" "$TOR_VIF" "$ip" "$vif"
      ;;
    offline)
      ip rule del from $ip lookup to_proxy
      clear_nft "$TOR_VIF" "$ip" "$vif"
      ;;
  esac
fi

What this does is create a default entry to the OpenBSD qube in the routing table, and configures routing on the firewall to ensure that clients use the to_proxy table. The script uses sed to create specific nftables rules in the qubes custom-forward chain, allowing traffic between the attached qube and the OpenBSD qube, and dropping any other traffic. If a client were to attempt to circumvent the default routing in some way, this will ensure this will fail.

The result is that qubes which have netvm set to the bsd_firewall will have all traffic routed through Tor, either by specifically setting SOCKS proxy to the IP of the OpenBSD Tor qube, or through the transparent proxy. Both provide circuit isolation between client qubes.

Testing

From a client on the same network, you can test DNS, SOCKS and the transparent proxy, using a simple script:

#!/bin/sh
  curl -x socks5h://X.X.X.X:9050 https://check.torproject.org |grep Congratulations
  curl https://check.torproject.org |grep Congratulations

Replace X.X.X.X with the IP address of the Tor proxy.

If successful you will see "Congratulations. This browser is configured to use Tor." for both tests. Notice that you do not need to specify a proxy address for the second curl test: the pf rules take care of that.