Tor Wifi Gateway

From Leo's Notes
Last edited on 1 September 2019, at 06:22.

Introduction

The Tor Wifi Gateway is a open wifi access point that lets users connect to the internet via Tor. The purpose of this is to create a gateway which transparently proxies traffic over to Tor (which intrinsically is useful for other projects), perhaps share internet access to neighbors, and perhaps provide access to my stash without authentication.

What Configuration
External Network Interface (to internet) eth0 (10.1.1.11)
Internal Network Interface (to wifi access point) eth1 (192.168.192.10, 192.168.192.11)

Setup

  1. Tor Setup: https://trac.torproject.org/projects/tor/wiki/doc/TransparentProxy#LocalRedirectionandAnonymizingMiddlebox1
  2. DHCP / DNS proxy for the wifi subnet

TOR

Edit the /etc/tor/torrc configuration to contain the following lines:

VirtualAddrNetwork 10.192.0.0/10
AutomapHostsOnResolve 1

TransPort 9040
TransListenAddress 192.168.192.10

DNSPort 53
DNSListenAddress  192.168.192.11

I've placed the DNS server on a separate IP because I will be using dnsmasq as my 'primary' DNS server that users will be using since I wanted my own portal/splash page with my own domain name (ie: torified.wifi).

Troubleshooting

If you are having trouble starting tor as a service with it complaining about being unable to listen on a specific port, ensure you have SELinux configured (or disabled).

DHCP / DNS Server

We will be using dnsmasq as the DHCP/DNS server.

listen-address=192.168.192.10
port=53

strict-order
no-resolv

# IPTables will route this back to 192.168.192.11.
# Doing this because dnsmasq refuses to listen to a local interface.
server=172.19.19.19

interface=eth1
address=/torified.wifi/192.168.192.10

dhcp-script=/scripts/dnsmasq_dhcp
dhcp-range=192.168.192.20,192.168.192.250,2h
dhcp-option=6,192.168.192.10
dhcp-option=3,192.168.192.10
no-dhcp-interface=eth0

domain=torified.wifi
log-queries
log-dhcp

# Define hosts
dhcp-host=00:0c:29:0f:a7:ab,win7vm,192.168.192.2,6h

For more information on DNSMasq, see Dnsmasq

Notice that the DNS server is 172.19.19.19 -- this IP does not in fact exist in my network. I will use IPTables (see below) to redirect this traffic to TOR. Reason I am using this IP is because it is not on the interface since dnsmasq will complain otherwise.

dnsmasq is configured to run the DHCP script at /scripts/dnsmasq_dhcp. This script basically ensures that the client gets 'unregistered' from the network when their lease times out or when they reconnect which will force them to my splash page. The contents of the script is quite simple:

#!/bin/sh

if [ $# -ne 3 -a $# -ne 4 ] ; then
        echo "Usage: dnsmasq_dhcp <add|del> <mac> <ip> [hostname]"
        exit
fi

TOR_IF="eth1"
Action=$1
IP=$3

# We want to remove them when they lose their lease...
if [[ "$Action" = "del" ]] || [[ "$Action" = "old" ]] ; then
        /sbin/iptables -t nat -D tor_clients -p tcp -s $IP --syn -j REDIRECT --to-ports 9040
        /sbin/iptables -t nat -D tor_clients -p udp -s $IP --dport 53 -j REDIRECT --to-ports 53
fi

IPTables

#!/bin/sh

IPT=/sbin/iptables
TOR_UID=109
TOR_NET=192.168.192.0/24
TOR_IF=eth1
OUT_IF=eth0

$IPT -F
$IPT -t nat -F

# Connections from WifiNet must go directly to the gateway.
# Nothing can go directly to the world.
$IPT -A FORWARD -i $TOR_IF -j REJECT

# This gateway will redirect gateway traffic through TOR
$IPT -t nat -X tor_clients
$IPT -t nat -N tor_clients

$IPT -t nat -A PREROUTING -i $TOR_IF -p tcp -d 192.168.192.10 --dport 80 -j ACCEPT
$IPT -t nat -A PREROUTING -i $TOR_IF -p tcp -d 192.168.192.10 --dport 53 -j ACCEPT
$IPT -t nat -A PREROUTING -i $TOR_IF -p udp -d 192.168.192.10 --dport 53 -j ACCEPT
$IPT -t nat -A PREROUTING -i $TOR_IF -p tcp -d 192.168.192.10 --dport 443 -j ACCEPT
$IPT -t nat -A PREROUTING -i $TOR_IF -p tcp -d 192.168.192.11 --dport 80 -j ACCEPT
$IPT -t nat -A PREROUTING -i $TOR_IF -j tor_clients
$IPT -t nat -A PREROUTING -i $TOR_IF -p tcp -j DNAT --to-destination 192.168.192.10:80

# Redirect dns traffic going to 172.19.19.19:53 to 192.168.192.11:53
# because dnsmasq refuses to listen to a local interface
$IPT -t nat -A OUTPUT -p udp -d 172.19.0.0/16 --dport 53 -j DNAT --to-destination 192.168.192.11:53
$IPT -t nat -A OUTPUT -p tcp -d 172.19.0.0/16 --dport 53 -j DNAT --to-destination 192.168.192.11:53

# Note: To route traffic for all hosts, use:
# $IPT -t nat -A PREROUTING -i $TOR_IF -p udp --dport 53 -j REDIRECT --to-ports 53
# $IPT -t nat -A PREROUTING -i $TOR_IF -p tcp --syn -j REDIRECT --to-ports 9040

# Allow specific hosts, bypass login screen
$IPT -t nat -I PREROUTING -i $TOR_IF -s 192.168.192.2 -p udp --dport 53 -j REDIRECT --to-ports 53
$IPT -t nat -I PREROUTING -i $TOR_IF -s 192.168.192.2 -p tcp --syn -j REDIRECT --to-ports 9040

$IPT -t nat -I PREROUTING -i $TOR_IF -s 192.168.192.3 -p udp --dport 53 -j REDIRECT --to-ports 53
$IPT -t nat -I PREROUTING -i $TOR_IF -s 192.168.192.3 -p tcp --syn -j REDIRECT --to-ports 9040

# To allow a new host, add the rules below for EVERY individual host:
# $IPT -t nat -I PREROUTING -i $TOR_IF -s $HOST -p udp --dport 53 -j REDIRECT --to-ports 53
# $IPT -t nat -I PREROUTING -i $TOR_IF -s $HOST -p tcp --syn -j REDIRECT --to-ports 9040

To add additional clients that can access the TOR network, run the following lines per IP:

$IPT -t nat -I tor_clients -p tcp -s $IP --syn -j REDIRECT --to-ports 9040 
$IPT -t nat -I tor_clients -p udp -s $IP --dport 53 -j REDIRECT --to-ports 53

To remove clients from access, run:

$IPT -t nat -D tor_clients -p tcp -s $IP --syn -j REDIRECT --to-ports 9040
$IPT -t nat -D tor_clients -p udp -s $IP --dport 53 -j REDIRECT --to-ports 53

Splash Page

With the basic infrastructure set up, we will now just need to create the splash page which will show the users the TOS, before letting them 'register' onto the network (which will just add their IP to the tor_clients_chain, thereby enabling internet access). The dnsmasq lease time will cause the IPs to 'unregister' after the lease expires when the /scripts/dnsmasq_dhcp script gets executed.

I wrote a quick PHP script that does this (far from perfect, but it works). You will need to create the actual content pages, but that should be quite trivial.

<?php
// The name of your splash page
define("SITE_NAME", "TORified.wifi");

// The secret 'key' used to generate tokens
define("SECRET", "s3kr3tz");

// Path to binaries
define("SUDO_EXEC", "/usr/bin/sudo");
define("IPTABLES_EXEC", "/sbin/iptables");

$secret = isset($_POST['secret']) ? $_POST['secret'] : NULL;
$ip = $_SERVER['REMOTE_ADDR'];


include "pages/header.php";

if ($secret === NULL) {
        include "pages/welcome.php";
} else {
        if (is_valid_key($secret)) {
                if (add_client($ip)) {
                        include "pages/registered.php";
                } else {
                        include "pages/error_registered.php";
                }
        } else {
                include "pages/error_timedout.php";
        }
}

include "pages/footer.php";

//
// Generates a key
//
function generate_key() {
        return md5(SECRET . date("Hi"));
}

//
// Verifies whether the key that was submitted on the form is valid.
// Keys expires after a minute.
//
function is_valid_key($secret) {
        $hour = date("H");
        $minute = date("i");

        return ($secret == md5(SECRET . $hour . $minute)
                || $secret == md5(SECRET . $hour . ($minute - 1))
                || $secret == md5(SECRET . ($hour - 1) . ($minute - 1)));
}

//
// Adds a new client to the network by allowing their IP to access TOR
//
function add_client($IP) {
        $rules = trim(shell_exec(SUDO_EXEC . " " . IPTABLES_EXEC . " -t nat -L|grep $IP | wc -l "));

        // If we have more than 2 rules for this IP, they are probably registered already...
        if ($rules < 2) {
                shell_exec(SUDO_EXEC . " " . IPTABLES_EXEC . " -t nat -I tor_clients -p tcp -s $IP --syn -j REDIRECT --to-ports 9040 2>&1");
                shell_exec(SUDO_EXEC . " " . IPTABLES_EXEC . " -t nat -I tor_clients -p udp -s $IP --dport 53 -j REDIRECT --to-ports 53 2>&1");

                return TRUE;
        }
        return FALSE;
}