The UMBC System Administration and Software Development (SAD) Club runs a pfSense VM with OpenVPN installed. For our internal splash page, I thought it would be kind of cool if it automatically determined, based on the request’s IP address, your username.
┌────────────┐ "who is 192.168.5.2?"
│ pfSense │◄────────────┐
│192.168.1.1 │ │
└────┬──┬────┴───────────┐ │
│||│ "it's nbura"│ │
│||│ ▼ │
┌────┴──┴────┐ ┌─────┴──────┐
│nbura on VPN├──────►│ welcome │
│192.168.5.2 │ │ page │
└────────────┘◄──────┴────────────┘
"welcome, nbura"
Serving VPN Data from pfSense
I’d need to get the IP→username associations out of pfSense somehow. My first thought was that pfSense must have some sort of API, but turns out there’s only 3rd party solutions. Disregarding that, I’d have to write up some sort of simple service to run on the pfSense box.
For starters, I enabled pfSense SSH to allow for easier development, then logged in as root. (Don’t judge…)
The OpenVPN Socket
On pfSense (and in general, I believe), OpenVPN actually runs a UNIX socket which functions as a simple management interface. Super cool.
It lives in /var/etc/openvpn/server2/sock
.
(Note that server2
is for my particular VPN of interest, yours may be server1
or otherwise.)
I used good old netcat (nc -U
) to access it, though there may be better alternatives.
On the TTY, this will give you an interactive interface. Type help
to get all the commands.
: ls -la /var/etc/openvpn/server2/sock
srwxrwxrwx 1 root wheel 0 Aug 22 22:34 /var/etc/openvpn/server2/sock
: cd /var/etc/openvpn/server2/
: nc -U sock
>INFO:OpenVPN Management Interface Version 3 -- type 'help' for more info
status
OpenVPN CLIENT LIST
Updated,2021-09-26 17:08:20
Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
nbura,REDACTED,7484004,19903436,REDACTED
ROUTING TABLE
Virtual Address,Common Name,Real Address,Last Ref
192.168.5.2,nbura,REDACTED,REDACTED
GLOBAL STATS
Max bcast/mcast queue length,0
END
You can script it by just echoing commands into nc -U
; again, I’m sure there’s better ways, but this worked.
Trouble is, the socket stays open. My awful solution was just to time it out.
(Note: nc -w
doesn’t take decimal numbers, and nc -W
doesn’t appear to exist on FreeBSD’s nc
.)
: echo status | timeout 0.05 nc -U /var/etc/openvpn/server2/sock
>INFO:OpenVPN Management Interface Version 3 -- type 'help' for more info
OpenVPN CLIENT LIST
...
Publishing with socat
I’ve heard great praise for socat
, but this was my first time actually trying it out.
To my understanding, it basically functions as a multipurpose “relay” for connecting network sockets to and from other sockets, shells, commands, etc.
A basic socat
setup to publish the VPN status to anyone on port 3100 looks as follows:
socat tcp-listen:3100,reuseaddr,fork system:"echo status | timeout 0.05 nc -U /var/etc/openvpn/server2/sock"
And you receive on the other end with:
$ nc -W 2 pfsense 3100
>INFO:OpenVPN Management Interface Version 3 -- type 'help' for more info
OpenVPN CLIENT LIST
...
The -W 2
causes it to quit after 2 packets have been received, so you get a nice, simple command that just outputs the status
table. Awesome.
NOTE:
socat
is explicitly used for connecting sockets together. One absolutely could just connectTCP-LISTEN
port 3100 directly to aUNIX-CONNECT
for the OpenVPN socket. However, I’d prefer to restrict this port to just running one single command, and any other user input should not be accepted; hence the weird approach withsystem
andnc
and all.
Anyways, while there’s nothing super crazy about this status
data and it is restricted to the internal network, it still publishes users' personal IPs,
which we’d prefer not to share among all users. So some security would be nice - albeit really sketchy security.
Securing socat
The solution was pretty simple. Where hunter2
is our password:
socat tcp-listen:3100,reuseaddr,fork system:"head -n 1 | grep -q '^hunter2' && echo status | timeout 0.05 nc -U /var/etc/openvpn/server2/sock"
The grep regex will verify that hunter2
is the first string on the first line. To access on the other end:
$ nc -w 1 -W 2 pfsense 3100
$ echo hunter2 | nc -w 1 -W 2 pfsense 3100
>INFO:OpenVPN Management Interface Version 3 -- type 'help' for more info
OpenVPN CLIENT LIST
...
END
$ echo aaahunter2 | nc -w 1 -W 2 pfsense 3100
$
Note the addition of the -w 1
to make the command time out after 1 second if authentication fails.
For more info on this… creative… setup, see this Gist.
Making a FreeBSD Service
This socat
service, which we’ll call catclients
, should obviously run by itself on boot.
⚠ WARNING: Despite a bit of research, I honestly wasn’t able to find much solid info on FreeBSD services. This solution is sloppy, cobbled together from various snippets, and I can’t recommend following it.
We create a file, /etc/rc.d/catclients
:
#!/bin/sh
# PROVIDE: catclients
# REQUIRE: LOGIN
. /etc/rc.subr
name="catclients"
rcvar=`set_rcvar`
command=/usr/local/bin/socat
thepassword="hunter2_insert_password_here"
command_args="tcp-listen:3100,reuseaddr,fork system:\"head -n 1 | grep -q '^$thepassword' && echo status | timeout 0.05 nc -U /var/etc/openvpn/server2/sock\""
start_cmd="/usr/sbin/daemon $command $command_args"
load_rc_config $name
run_rc_command "$1"
Start (service catclients start
) and enable it. We should be good to go.
Fetching the Data
With the server setup complete on pfSense’s end, it’s time to set up the client on the hello
box.
For simplicity’s sake, we’ll just keep using netcat rather than set up the socket ourselves.
Some shell pipelining helps easily strip down the OpenVPN output.
$ echo hunter2 | nc -w 2 pfsense 3100
>INFO:OpenVPN Management Interface Version 3 -- type 'help' for more info
OpenVPN CLIENT LIST
Updated,2021-09-26 19:19:40
Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
nbura,REDACTED,14404523,32804708,2021-09-26 02:10:16
ROUTING TABLE
Virtual Address,Common Name,Real Address,Last Ref
192.168.5.2,nbura,REDACTED,2021-09-26 19:19:40
GLOBAL STATS
Max bcast/mcast queue length,0
END
$ echo hunter2 | nc -w 2 pfsense 3100 | grep -A 9999 ROUTING
ROUTING TABLE
Virtual Address,Common Name,Real Address,Last Ref
192.168.5.2,nbura,REDACTED,2021-09-26 19:19:48
GLOBAL STATS
Max bcast/mcast queue length,0
END
$ echo hunter2 | nc -w 2 pfsense 3100 | grep -A 9999 ROUTING | grep -B 9999 GLOBAL
ROUTING TABLE
Virtual Address,Common Name,Real Address,Last Ref
192.168.5.2,nbura,REDACTED,2021-09-26 19:19:57
GLOBAL STATS
$ echo hunter2 | nc -w 2 pfsense 3100 | grep -A 9999 ROUTING | grep -B 9999 GLOBAL | tail -n +3
192.168.5.2,nbura,REDACTED,2021-09-26 19:20:03
GLOBAL STATS
$ echo hunter2 | nc -w 2 pfsense 3100 | grep -A 9999 ROUTING | grep -B 9999 GLOBAL | tail -n +3 | head -n -1
192.168.5.2,nbura,REDACTED,2021-09-26 19:20:07
This is represented in the Elixir backend code as:
{output, _} =
System.shell(
"echo #{catclients_pw}" <>
" | nc -w 2 pfsense 3100" <>
" | grep -A 9999 ROUTING" <>
" | grep -B 9999 GLOBAL" <>
" | tail -n +3" <>
" | head -n -1"
)
And finally, we get the row with their IP address and parse out the username:
# determine IP of incoming request
user_ip = to_string(:inet_parse.ntoa(conn.remote_ip))
# parse as CSV and take the row with the matching ip
row =
output
|> CommaParser.parse_string(skip_headers: false)
|> Enum.filter(fn row -> Enum.at(row, 0) == user_ip end)
|> Enum.at(0)
# print the name of the connecting user
username = Enum.at(row, 1)
Logger.info(username)
With that, we’re done! The username is printed onto the console when they load the webpage.
Finally, in the real app, we make an LDAP query on their username to determine their full name, password status, groups, and email.