The normal way to change IPs
It's very handy as a programmer to change your public facing IP. Whether you want to pretend your computer is in a different country or just want to get around an IP ban, being able to masquerade your IP has many uses.
The workflow I've always used to achieve this is:
- running
openvpn
as a background process - do what I want to do
- kill the
openvpn
process
When I was testing some IP banning tooling I was using this workflow and
quickly came across the problem that switching locations/IPs multiple times
gets annoying as I have to kill and start openvpn
multiple times. So I
wanted to build a utility to make it easier by collapsing this workflow into
just one step.
The ideal experience I wanted was to run a command isolated from the rest of my local network, yet be connected to a VPN whose location I can change with a single argument.
Coding it up
Since I wanted isolation for this tool, I chose podman
as it's my goto
container engine for my one off isolation needs. The simplest place I started
was with a quick script to build an ovpn
alpine image like so:
1NAME=ovpn2cat << EOF > /tmp/Dockerfile.${NAME}3FROM alpine:3.124RUN apk add --no-cache openvpn bash5EOF6EMPTY=$(mktemp -d)7podman build -q -t ${NAME} -f /tmp/Dockerfile.${NAME} $EMPTY 2>&1 > /dev/null
The immediate problem I saw with this approach was that the container image doesn't include any of my local tools. It didn't seem practical to keep adding dependencies as needed to this image. This would cause image bloat and I would have to rebuild the image any time I want to add something.
I wanted a way to run this image as a conduit for my other tools.
The solution I eventually reached was to simply run the isolated openvpn
container, then run a command I wanted in the containers network
namespace. That way I get the network isolation I want, with the ability to
isolate the entire execution of a command behind a VPN.
To do this I ran the container with privileges to create a tun
interface
like so:
1podman run --rm --cap-add NET_ADMIN,NET_RAW --device /dev/net/tun \2 --name ${CONTAINER_NAME} \3 -v /tmp/ovpn/:/tmp/ovpn/ \4 -it -d ${NAME} \5 bash -c "6 openvpn \7 --config $FILE \8 --auth-nocache \9 --auth-retry nointeract \10 --dev $tun \11 --dev-type tun \12 --errors-to-stderr \13 --auth-user-pass <(echo -e '$USERNAME\n$PASSWORD')14 " 1>&2
Then I used nsenter
to run a command in the context of the containers
network namespace:
1...2PID=$(podman inspect ${CONTAINER_NAME} | jq -r '.[0] | .State.Pid')3nsenter -U -n -t $PID $@
This worked just as expected, and when the command finishes, the container can be stopped and removed for seamless cleanup.
Hooray containers and namespaces! 📦🎊
Wrapping this all together in a script run_behind_vpn
, I can now run a
command masquerading as different IPs like so:
1#!/usr/bin/env bash2export USERNAME=$USERNAME3export PASSWORD=$PASSWORD4export US=$(find /tmp/ovpn/*us* -print -quit) # find a US ovpn file5export JAPAN=$(find /tmp/ovpn/*jp* -print -quit) # find a JP ovpn file6run_behind_vpn $US -- curl -s ipinfo.io | jq -r '.country'7run_behind_vpn $JAPAN -- curl -s ipinfo.io | jq -r '.country'
1US2JP
Now if I ever need to pretend that I'm running my computer from a different country or need a new IP, I have a method to do so.
Try the script out for yourself here and thanks for reading!