Re-Writing a GlobalProtect OpenConnect VPN Connect script in Babashka
Table of Contents
**
- Edit 2023.001.17: fixed syntax errors CLI command in
/usr/bin
andusername > user
This is an update of a previous Babashka1 project2, changed because my organization has switched to a Palo Alto GlobalProtect VPN instead of our former Cisco one. Originally this posed a major problem for OpenConnect, which had troubles with GlobalProtect, but with wersion 9+ of OpenConnect it works smoothly (as long as your provider doesn’t require SAML authentication, which is apparently still buggy in OpenConnect 9).
Here is the one-line, two-command and four-option CLI version3
echo "my VPN Password" | sudo openconnect --csd-wrapper=/usr/libexec/openconnect/hipreport.sh --protocol=gp global-protectvpn.organization.edu --user=organization-person@organization.edu --passwd-on-stdin
After running that hefty command I am prompted for my sudo password (maybe), then will receive a ping for my organization’s two-factor authentication on my phone; once I accept that, I am online. Now for the Babashka version, which is a much more elegant command and includes features like quit
, restart
, and status
.
Install Babashka
bash < <(curl -s https://raw.githubusercontent.com/babashka/babashka/master/install)
Yep. That’s all. Now you have bb
as a command on your system and can use it to run clojure code.
Spoilers: final product
Since you have Babashka now, just stick this final product in your script file (note the #!
line at the beginning telling it to use babashka). You can find the full source on gitlab4.
The Credentials File
The default credentials file lives on a linux system at ~/.vpncred.edn
. The format edn
is the same format as Clojure code, and can be considered JSON’s big-brother. You will notice that the new GlobalProtect one no longer has a :authgroup
tab, and has replaced the obscure :servercert
with a :csd-wrapper
that needs to be to a file within an OpenConnect9+ install, usually located in /usr/libexec/openconnect
5 This file, given secure file permissions, has a structure like this:
;; ~/.vpncred.edn
{:username "USER"
:password "PASS"
:vpn-server "VPN.COM"
:csd-wrapper "/usr/libexec/openconnect/hipreport.sh"}
Clearly a plaintext password, even in a file with closed permissions, is less than ideal. Someday I might get around to using gpg or some other encryption method.
Principles
At the beginning, and indeed for most or the script, you can just write it like a bash script, using java’s sh
shell interop. As the Babashka documentation says, this handles about 90% of use cases. It works great for things that are run-and-done: our “stop” and our “status” functionality, in particular. However, since our VPN has a long-running daemon as long as it’s active, we need to delve into Babashka’s Process functionality.6 We use this both to separate a spawned process from our program (the OpenConnect process), and to pipe to that process our credentials along stdin. In particular, that’s the (_connect)
function here:
(defn _connect [&[credfile]]
"Connect to the VPN using openconnect, then automatically provide it your password on stdin.
May then require Two Factor Authentication per if org enabled."
(let [{:keys [username password vpn-server csd-wrapper]} (credentials credfile)
csd-phrase (str "--csd-wrapper=" csd-wrapper)
user-phrase (str "--user=" username)
protocol-phrase "--protocol=gp"
openconnect (cond->
["sudo" "openconnect" "--background" csd-phrase protocol-phrase vpn-server user-phrase]
:finally (conj "--passwd-on-stdin"))
proc @(p/process openconnect {:out :inherit
:err :inherit
:in password})]
(println "command-line is:\n" openconnect)
proc))
Some notes on this:
- The credfile is optional because the
credentials
function will use its default if none is given. - The openconnect command string uses
cond->
leftover from the previous versionto add an optional--servercert
line if included in the credentials - It uses
p/process
instead ofsh
to create a process that won’t close when our babashka script itself finishes. - It uses
@(p/process)
in order to reify it in time for there to actually be a stdin to which the password will be written.
Interface
One of the major reasons I wanted to use Babashka was because writing the api was so tedious in Bash. With the help of Clojure’s parse-opts, this became pleasant and well-organized in Babashka. One fact worth learning was the shell difference between options7 and arguments.8 Importantly, arguments come in as an ordered vector of space-separated things, while options come in all at once as a map of provided options. I make use of the :default
option to specify the credential file whether or not one is given (although I also demonstrate another way of enforcing defaults elsewhere through the code).
Footnotes
1 Babashka, easily-installed, performant shell-scripting using the well-designed Clojure language: https://github.com/babashka/babashka .
2 The previous post with the Cisco version is at https://tech.toryanderson.com/2021/03/06/re-writing-an-openconnect-vpn-connect-script-in-babashka/ . Full series on this OpenConnect wrapping process here: https://tech.toryanderson.com/tags/openconnect/ .
3 Echoing and piping to give OpenConnect your password in Bash: https://askubuntu.com/questions/1043024/how-to-run-openconnect-with-username-and-password-in-a-line-in-the-terminal
4 Full and latest source here: https://gitlab.com/toryanderson/bbvpn . I like gitlab as an alternative to the Microsoft-owned Github because alternatives are good.
5 The guix version of that location, since the root is kept pretty sparse, is on your local user at, eg, "/home/myuser/.guix-profile/libexec/openconnect/hipreport.sh"
6 Process docs, including comparison against plain shell sh
, here: https://github.com/babashka/process .
7 Yes, it turns out there is a crucial difference in shell scripting! Options are preceded by hyphens, like “–status” (long form) or “-s” (short form), while arguments are simple appendages, like echo ARGUMENT
.
8 Options start with “-” or “–” for short and long options, while arguments are the plain space-separated directives that follow your root command. I provided the option -c
or --credentials
for specifying a non-default credential file, and take arguments start
, stop
, restart
, or status
. You can read all about this and more functionality at https://github.com/clojure/tools.cli . With OpenConnect it is important to realize that the short options do NOT have a space or an equal sign following; eg -uMyUser
.