Whose IP's are these anyway? - splitting a VPN by target
I’ve been quite happy with my poor man’s VPN using sshuttle, which allowed tunneling traffic from any device on a local VLAN through a VPN. So far I had an all-or-nothing approach, where devices in the VLAN had all of their traffic sent through the VPN. However, I’ve recently hit a case where I had a single device that needed both VPN and regular routing (don’t ask…). This article explores how to update the sshuttle setup, allowing for mixed routing, where traffic to some destinations gets routed through the VPN tunnel, but other traffic bypasses the tunnel.
What’s the IP, Kenneth?
NOTE: If you just happen to know the destination IP networks, you can skip this section and just jump ahead to the sshuttle configuration.
In my case, the destination I wanted to single out wasn’t a single known address or subnet, but rather some large ISP that owns a large number of possibly changing address ranges. Let’s use http://sunrise.ch (former UPC) as an example for this article.
So, how do we discover what IP addresses sunrise.ch owns? Like most things on the internet, the IP Address space is managed in a distributed fashion. To quote Wikipedia: “The IP address space is managed globally by the Internet Assigned Numbers Authority (IANA), and by five regional Internet registries (RIRs) responsible in their designated territories for assignment to local Internet registries”. Given sunrise.ch is in Switzerland, http://www.ripe.net/ is the regional internet registry responsible for assigning IP addresses.
TIP: If your destination isn’t an ISP or something huge like Amazon, Google, etc. chances are they get their IP address ranges from their internet service provider (like Sunrise) instead of directly from RIPE. So if you can’t find any entries for your target at RIPE you might need to use other ways like seeing what DNS resolves to for the domains you are interested in.
RIPE offers the RIPE Database which can be queried through a Web Frontend and a REST API, or whois. The main purpose of the database is lookups along the lines of “I have this IP, who owns that?”. For example:
# Find some IP address that should belong to Sunrise
$ dig sunrise.ch
...
;; ANSWER SECTION:
sunrise.ch. 300 IN A 212.35.60.35
# Query RIPE through whois, which confirms 212.35.60.35 is
# part of an IP range assigned to Sunrise (AS6730)
$ whois 212.35.60.35
inetnum: 212.35.60.0 - 212.35.60.255
netname: SUNRISEIT-NET
descr: Sunrise Communications AG
country: CH
...
status: ASSIGNED PA
mnt-by: AS6730-MNT
...
% This query was served by the RIPE Database Query Service ...
Which is great, but not exactly what we wanted. Thankfully the database
also supports inverse queries
using the -i
flag. While there is no support for directly querying by name (the netname) above,
it does support querying by mnt-by (mb)
, which records the
Autonomous System (AS) that
maintains a given IP range. Given we already learned earlier that Sunrise is AS 6730
the query -r -i mb AS6730-MNT
should find intentnum records that belong to Sunrise.
I found that using the whois
command-line tool seemed to truncate the response,
while a bare-metal query with netcat returns complete results (good that whois is such
a simple protocol…):
# Connect to whois.ripe.net and send the query
# "-r -i mb AS6730-MNT -K" (port 43 is whois).
$ nc whois.ripe.net 43 ⏎
-r -i mb AS6730-MNT -K ⏎
% This is the RIPE Database query service.
...
inetnum: 145.250.0.0 - 145.250.127.255
inetnum: 158.133.0.0 - 158.133.255.255
inetnum: 178.38.0.0 - 178.38.63.255
inetnum: 178.38.0.0 - 178.39.255.255
inetnum: 178.38.128.0 - 178.38.191.255
inetnum: 178.38.192.0 - 178.38.255.255
inetnum: 178.38.64.0 - 178.38.127.255
inetnum: 178.39.0.0 - 178.39.63.255
inetnum: 178.39.128.0 - 178.39.191.255
inetnum: 178.39.192.0 - 178.39.217.255
inetnum: 178.39.218.0 - 178.39.238.255
inetnum: 178.39.239.0 - 178.39.248.255
inetnum: 178.39.249.0 - 178.39.249.255
inetnum: 178.39.250.0 - 178.39.255.255
inetnum: 178.39.64.0 - 178.39.127.255
...
NOTE: You will probably want to specify the
-r
(--no-referenced
) flag in your queries, which suppresses returning referenced information like the organisation, person and role objects which are subject to daily limits. Without this, I found myself blocked after a handful of queries as inverse queries tend to return a lot of records.-K
asks to only return primary keys and can help further reduce load and bandwidth needs.
A glass of CIDR?
There’s one more small issue to solve. RIPE returns IP address ranges like 178.38.64.0 - 178.38.127.255
while sshuttle expects the slash notation established for CIDR. So our example here would need to become 178.39.64.0/18
.
The slash notation is a shorter way to specify a network prefix. The
/18
here tells us that the first 18 bits of the address identify the network. Another way to write this would be178.39.64.0/255.255.64.0
We can convert ranges to the slash notation by comparing the addresses in binary format, finding their common prefix. Here’s the relevant part in Python:
def common_prefix(a, b):
"""Finds the common (bit-)prefix of two (4-byte) integers.
For example, for inputs 0xFFFFF000 and 0xFFF00000 would
return (0xFFF00000, 12).
Returns:
a tuple (prefix, len) with only the common prefix kept and
the bit-length of that prefix.
"""
out = bytearray()
# Find the most significant bit that differs
prefixlen = 32
mask = 0xFFFFFFFF
while prefixlen > 0 and (a & mask) != (b & mask):
prefixlen -= 1
mask <<= 1
out = a & mask
return (out, prefixlen)
def ip_range_to_subnet(ip_range):
"""Converts an IP range string to subnet notation.
Args:
ip_range: A string like 178.39.64.0 - 178.39.127.255
Returns:
An IPv4Network like IPv4Network('178.39.64.0/18')
"""
a, b, *c = ip_range.split("- ")
start = ipaddress.IPv4Address(a.strip())
end = ipaddress.IPv4Address(b.strip())
net = ipaddress.IPv4Network(
common_prefix(
int.from_bytes(start.packed, byteorder='big'),
int.from_bytes(end.packed, byteorder='big')))
Putting this all together, we can write a script like lookup_asn_subnets.py to query ripe.net and convert the ranges to subnet notation:
$ ./lookup_asn_subnets.py 6730
145.250.0.0/17
158.133.0.0/16
178.38.0.0/18
...
Configuring sshuttle
All that’s left is to feed these subnets to sshuttle. That supports reading subnets from
a file using the -X <file>, --exclude-from=<file>
flag
$ ./lookup_asn_subnets.py 6730 > /tmp/subnets.txt
$ sshuttle ... --exclude-from=/tmp/subnets.txt
And with that, we have a split VPN tunnel, where traffic to some subnets skips the tunnel while other traffic gets sent through.