Introduction

In previous note I’ve showed how to run OpenVPN client inside of the Docker container. Today, we will prepare makefile to automate establishing connectivity.

Functions

Makefile should:

  • verify if configuration is in place
  • connect to VPN
  • disconnect from VPN
  • configure credentials (if required)

Makefile development

Let’s start development with simple, empty file:

touch makefile

Variables declaration

A Makefile can handle variables, and I find it useful to declare some of them at the beginning of the file. In a Makefile, you can declare variables using =, :=, or ?=. What is the difference between them?

= - evaluates right hand of the expression when variable is used. := - evaluates right hand of the expression immediatelly. ?= - it is conditional assigment - It assigns value only, if variable was not set earlier.

Example:

apples = I like ${colour} apples
bananas := Bananas are not ${colour}
colour ?= green
colour ?= red

all:
    echo ${apples}
    echo ${bananas}
    echo ${colour}

Output:

echo I like green apples
I like green apples
echo Bananas are not 
Bananas are not
echo green
green

I like green apples - Value of variable colour was injected into this string. Bananas are not - Value was not injected, because it was evaluated immediately, and it was not set yet. green - Assigning red to the variable doesn’t work, because it was already defined.

With this basic knowledge about variables, lets jump in into makefile development.

.DEFAULT_GOAL := help
VPN_VERSION ?= latest
VPN_CONFIG_PATH ?= ${PWD}/vpn/LAN2_VPN.ovpn
PASSFILE_PATH ?= ${PWD}/vpn/passfile
USERNAME ?= $(shell bash -c 'read -p "Username: " username; echo $$username')
PASSWORD ?= $(shell bash -c 'read -s -p "Password: " pwd; echo $$pwd')
VPN_DOCKER_IMAGE ?= myregistry/vpn_client

.DEFAULT_GOAL := help - This is default goal that will be executed when you run make command. I want to display help if no parameter is provided. I am using := here, as I don’t want to change it using envs. All other variables are assigned using ?= because it is possible to overwrite them using envs. VPN_VERSION - image tag that will be used to establish connection. I am using latest tag here. VPN_CONFIG_PATH - path to OpenVPN config. Default it is located in vpn directory, in same location as makefile. PASSFILE_PATH - If you are using username and password to authenticate, you will need passfile to establish connection. This is path to it. USERNAME and PASSWORD are in fact commands to get them. You can also use envs to set these variables. VPN_DOCKER_IMAGE - docker image that will be used.

In makefile you should use .PHONY if your task is not producing any valid file. Good example for it is help message:

.PHONY: help
help:  ## Display this help message
	 @egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

It displays help message using comments in makefile to construct the message.

You might also need task to configure passfile:

credentials:  ## Get VPN credentials and write to passfile
	@echo $(USERNAME) > ${PASSFILE_PATH}
	@echo $(PASSWORD) >> ${PASSFILE_PATH}

It takes USERNAME and PASSWORD variables and put them into PASSFILE_PATH. When you execute this task, you will be asked for username and password.

Next task is connect:

connect: stop verify start  ## Start new connection to Open VPN executing stop -> verify -> start

This task is executing three other tasks sequencialy: stop (stoping other vpn connection if exists), verify (checks if config file is in place), start (finally starts the connection)

Starting vpn connection (start task):

.PHONY: start
start:  ## start docker container that establishes connection to VPN
        @docker run --rm -it -p 10022:10022 -p 443:443 -p 9001:9001 \
  --cap-add=NET_ADMIN --cap-add=SYS_MODULE --device /dev/net/tun \
  -v ${VPN_CONFIG_PATH}:/vpnconfig.ovpn \
  -v ${PASSFILE_PATH}:/passfile \
  --name vpn \
  -d ${VPN_DOCKER_IMAGE}:${VPN_VERSION}

Let’s go with this command step by step:

@docker run --rm -it -p 10022:10022 -p 443:443 -p 9001:9001 \

We are running docker image with --rm parameter to clean it up, when we stop VPN. -it is used to make it interactive. We are also forwarding few ports for services we are going to access: -p 10022:10022 -p 443:443 -p 9001:9001.

--cap-add=NET_ADMIN --cap-add=SYS_MODULE --device /dev/net/tun \

To allow unprivigiled container to access network stack and load and unload kernel modules I am adding capabilities NET_ADMIN and SYS_MODULE. --device /dev/net/tun allows access network tun device.

-v ${VPN_CONFIG_PATH}:/vpnconfig.ovpn \
-v ${PASSFILE_PATH}:/passfile \

Our vpn image requires vpn config and passfile in / location. These two lines are putting files in the right place.

--name vpn \

Naming container, just to be more user friendly :)

-d ${VPN_DOCKER_IMAGE}:${VPN_VERSION}

Finally, I am running container in background and proper image name and tag.

To stop vpn, I’ve added stop task:

stop:  ## stop docker container and vpn connection
        @docker stop vpn || true

It simply stops container. || true is added to bypass error if container does not exist.

verify step:

.PHONY: verify
verify:  ## verify if config file and passfile are in place
        @if ! [ -f ${PASSFILE_PATH} ]; then echo "${PASSFILE_PATH} doesn't exist!"; exit 1; fi
        @if ! [ -f ${VPN_CONFIG_PATH} ]; then echo "${VPN_CONFIG_PATH} doesn't exist!"; exit 1; fi

Those are basically two simple checks if files exist in configured location. If something is missing, makefile will fail.

Cleanup steps:

cleanCreds:
        @rm -f ${VPN_CONFIG_PATH} ${PASSFILE_PATH}

cleanConf:
        @rm -f ${VPN_CONFIG_PATH}

cleanAll: stop cleanConf cleanCreds ## clean (remove) docker image and config files.
        @docker rmi ${VPN_DOCKER_IMAGE}:${VPN_VERSION}

They are very simillar: cleanCreds and cleanConf are removing config file (cleanConf) and config file and passfile (cleanCreds). cleanConf is executing tasks in order: stop -> cleanConf and cleanCreds. So it stops vpn and removing config file and passfile.

Full makefile

.DEFAULT_GOAL := help
VPN_VERSION ?= latest
VPN_CONFIG_PATH ?= ${PWD}/vpn/FSE_LAN2_VPN.ovpn
PASSFILE_PATH ?= ${PWD}/vpn/passfile
USERNAME ?= $(shell bash -c 'read -p "Username: " username; echo $$username')
PASSWORD ?= $(shell bash -c 'read -s -p "Password: " pwd; echo $$pwd')
VPN_DOCKER_IMAGE ?= myregistry/vpn_client


.PHONY: help
help:  ## Display this help message
	 @egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

credentials:  ## Get VPN credentials and write to passfile
	@echo $(USERNAME) > ${PASSFILE_PATH}
	@echo $(PASSWORD) >> ${PASSFILE_PATH}

connect: stop verify start  ## Start new connection to VPN executing stop -> verify -> start

.PHONY: start
start:  ## start docker container that establishes connection to VPN
	@docker run --rm -it -p 10022:10022 -p 443:443 -p 9001:9001\
  --cap-add=NET_ADMIN --cap-add=SYS_MODULE --device /dev/net/tun \
  -v ${VPN_CONFIG_PATH}:/vpnconfig.ovpn \
  -v ${PASSFILE_PATH}:/passfile \
  --name vpn \
  -d ${VPN_DOCKER_IMAGE}:${VPN_VERSION}

.PHONY: stop
stop:  ## stop docker container and vpn connection
	@docker stop vpn || true

.PHONY: verify
verify:  ## verify if config file and passfile are in place
	@if ! [ -f ${PASSFILE_PATH} ]; then echo "${PASSFILE_PATH} doesn't exist!"; exit 1; fi
	@if ! [ -f ${VPN_CONFIG_PATH} ]; then echo "${VPN_CONFIG_PATH} doesn't exist!"; exit 1; fi

cleanCreds:
	@rm -f ${VPN_CONFIG_PATH} ${PASSFILE_PATH}

cleanConf:
	@rm -f ${VPN_CONFIG_PATH}

cleanAll: stop cleanConf cleanCreds ## clean (remove) docker image and config files.
	@docker rmi ${VPN_DOCKER_IMAGE}:${VPN_VERSION}

Additional reading