Re-Writing an OpenConnect VPN Connect script in Babashka

Table of Contents

img

This is the second of a 2-part series of articles.1 It was updated 2021.005.24 to add the :servercert option to the openconnect command.

The sundries of just handling arguments and options2 was alone so annoying in Bash that I finally used it as an excuse to play with Babashka3, which I’ve been watching eagerly for quite a while. The script itself is small while providing niceties that would have taken much longer, and also more lines of code, to implement in Bash. In particular one of our goals is to rework more-securely the old bash weirdness to provide a password to openconnect4:

echo mypassword | openconnect --protocol=anyconnect --user=myusername --passwd-on-stdin 

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 gitlab5.

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. This file, given secure file permissions, has a structure like this:

;; ~/.vpncred.edn
{:username "USER"
 :password "PASS"
 :vpn-server "VPN.COM"
 :authgroup "SOME AUTHGROUP"
 :servercert "pin-sha256:somethingsomething"}

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"
  (let [{:keys [username password vpn-server authgroup servercert]} (credentials credfile)
        userphrase (str "--user=" username)
        groupphrase (str "--authgroup=" authgroup)
        openconnect (cond->
                        ["sudo" "openconnect" vpn-server "--background" groupphrase userphrase]
                      servercert (into ["--servercert" servercert])
                      :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-> 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 options and arguments7. 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 Full series on this OpenConnect wrapping process here: https://tech.toryanderson.com/tags/openconnect/ .

2 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.

3 Babashka, easily-installed, performant shell-scripting using the well-designed Clojure language: https://github.com/babashka/babashka .

4 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

5 Full and latest source here: https://gitlab.com/toryanderson/bbvpn . I like gitlab as an alternative to the Microsoft-owned Github because alternatives good.

6 Process docs, including comparison against plain shell sh, here: https://github.com/babashka/process .

7 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 .

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