Re-Writing a GlobalProtect OpenConnect VPN Connect script in Babashka

Table of Contents

img **

  • Edit 2023.001.17: fixed syntax errors CLI command in /usr/bin and username > 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/openconnect5 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 version to add an optional --servercert line if included in the credentials
  • It uses p/process instead of sh 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.

Tory Anderson avatar
Tory Anderson
Full-time Web App Engineer, Digital Humanist, Researcher, Computer Psychologist