Hey everyone! today I’d like to tell you a small story about a guy that’s been stuck working from home for more than a year and is suffering from constant VPN issues. This guy is me!
TLDR: can’t access corporate resources? use static routing to redirect connections through the corporate VPN.
Ok, so the story goes like this: I work at a Big-Corp & my team has a couple of VMs deployed on Azure which are governed by an internal service that makes sure that nobody can access them outside the corporate VPN. The service thus a bunch more things, but the gist is that one can’t alter the network security group (“firewall” rules) such that outsiders can access the machine.
That’s pretty neat, but problematic when you work from home. Why? not all network originating from our machine goes through the corporate network. In other words, when I try to access any resource that belongs to my team, the IP that’s being used is the one that belongs to my ISP –> forbidden.
How do I know that? my laptop’s routing table. The routing table is in charge of mapping the network routes for a given destination IP. On my macOS machine the routing table looks something like this:
$ netstat -rn |
I truncated some values, but the gist is: any traffic going to 127.0.0.0/8 (255.0.0.0) -> use the lo0
interface, and everything else go to the default, which is en0
or 10.0.0.1
. If I turn on the VPN, a new virtual network device appears (a.k.a: tun device) and many (many!) more routes show up:
$ netstat -rn |
What basically happens is that the VPN client connects to the vpn server, creates a secure tunnel between my computer and the VPN server, fetches a policy that dictates which IPs and subnets should go through the corporate network and configures my machine to send such traffic through the virtual interface.
When a packet hits the virtual interface, it gets encrypted and sent over the public internet to the VPN server. The VPN server has two “legs”: one in the corporate network and one in the public network. Once a client connects, every (verified) packet that arrives to the VPN server on the public internet gets sent to the corporate network.
In our case, we have an azure resource that can only be accessed through the corporate network.
we want to access a specific azure resource that is not configured in the VPN policy. There are two solutions: either fix it locally or use a jumpbox.
A jumpbox is a machine that has access to both networks: the public one and the corporate one. I can connect to it (using ssh, rdp, whatever) and than connect to the remote resource. SSH even has a flag for that (-J
or ProxyJump
):
# We connect to the jumpbox than the jumpbox connects to the remote-pc, |
Hack, why should we need another PC? why not just route traffic to the VPN? should be easy. well, it is, all we need to do is a simple static route:
$ sudo route -n add -net <remote-pc> <vpn-gateway-ip> |
That’s easy enough, but I honestly don’t want to add a static route everytime I want to connect to a remote resource. ITS A PAIN IN THE ASS! Why not write script to do that for me?
Well, I need a few things:
First, I need a way to extract the gateway IP address. the problem? GlobalConnect uses a different IP and with a different interface name everytime it connects!
Second, I want a robust script that has the same completion as ssh does.
Let’s tackle the first issue: I guess that GlobalConnect has logs somewhere or a configuration that specifies the interface and ip address it used to create the tun device. in macOS this stuff should sit at /Library
. I grepped for the VPN server address and viola!
/Library/Preferences/com.paloaltonetworks.GlobalProtect.settings.plist
contains the VPN configuration:
<?xml version="1.0" encoding="UTF-8"?> |
God! this is a piece of shit. I hate parsing xml… maybe there’s something that can parse plist files? YESSSS!!! /usr/libexec/PlistBuddy
does just that! it’s like jq but for plist!
$ GP_CONFIG=/Library/Preferences/com.paloaltonetworks.GlobalProtect.settings.plist |
Turns out there’s another way. I also grepped the vpn gateway IP and found it at /Library/Logs/PaloAltoNetworks/GlobalProtect/network/config/itf-install.sh
.
GlobalConnect writes down all the configuration to the logs directory, including the scripts it uses to configure the routes. the itf-install.sh
contains this:
/sbin/ifconfig utun2 10.10.5.125 10.10.5.125 netmask 255.255.255.255 up |
Awesome! I can just awk it, but I do need root permissions :(
$ ITF_INSTALL=/Library/Logs/PaloAltoNetworks/GlobalProtect/network/config/itf-install.sh |
Anyway, once that’s working, what about completion? well, in fish shell this is pretty straight forward. All I had to do is find the completion file for ssh
and just copy it for my script. I called my script vssh
and the ssh
completion file sat here: /opt/local/share/fish/completions/ssh.fish
. I copied it to ~/.config/fish/completions
, replaced ssh
with vssh
and viola! I had tab completions :)
Let’s start writing down our script… the skeleton should look like this:
#!/usr/bin/env fish |
this is pretty straight forward, but two things are missing: how do I find out the hostname if I’m passing arbitrary ssh args? and second, how will this work with ssh-config?
ssh -G
to the rescue! the -G
flag causes ssh to print its configuration after evaluating Host and Match blocks. I can use it to find out the actual hostname that ssh
is going to use in order to connect to the remote host!
set remote (ssh -G $argv 2>/dev/null | awk '/^hostname/ { print $2 }') |
cool, right?! That’s all folks! I’ve uploaded the whole script to my GitHub. as always, feel free to comment :)