Mass-Configuring OVPN in NetworkManager (on Ubuntu 18.04LTS)

So, I had a pretty large problem to figure out: How to configure 170+ different OVPN (Swedish/English) configurations in NetworkManager, without having to enter my username and password over 170 friggin’, fraggin’ times.

(NOTE: If you’re highly security conscious, then this probably isn’t going to be very palatable for you. You have been warned.)

I use [REDACTED] VPN service and they offer a zip file that you can download with containing multiple OVPN configuration files; a unique file for each server that you can use. After filtering the list down to my targets, I had a resultant list to import into Network Manager.

In order to have NetworkManager leverage ovpn, though, you need to install two packages, first.

sudo apt-get install network-manager-openvpn network-manager-openvpn-gnome

Bes sure to restart NetworkManager, afterwards, so you don’t end-up in a dissonant state.

sudo service network-manager restart

So, how do we do that? Well, it’s not as complicated as one my think and you needn’t go crafting anyting in Python or the like to do so.

for i in <location of your ovpn files>; do sudo nmcli connection import type openvpn file "$i"; done

Now that we have the files imported, we need to modify them. To do this, the easiest way is to use Python to read in the files and write our changes. Modify the script below to include your username and password.

#!/usr/bin/env python
"""Changes the exported OVPN configurations in NetworkManager to contain the username and password and autoconnect.

When mass-importing OVPN configuration files, it's necessary to overcome the hurdle of the prompt for passwords. This
script looks for the exported configuration files in NetworkManager. It writes the password configuration and the
username/password to the file. You will need to restart NetworkManager for the changes to be imported; however, it's
easier/better just to bounce the machine. (sudo init 6)

TO RUN:
    sudo python3 ModifyOvpnConfigurations.py
"""

__author__ = "felsokning"
__copyright__ = "Copyright 2019"
__license__ = "MIT"

import os
import re

source_directory = "/etc/NetworkManager/system-connections"
ovpn_files = os.listdir(source_directory)
i = 0
for ovpn in ovpn_files:
    opened_file = open(source_directory + "/" + ovpn, "r+")
    file_content = opened_file.read()
    if file_content.__contains__("mssfix=1450\n"):
        output = re.sub(r"mssfix=1450", r"mssfix=1450\npassword-flags=0", file_content)
        # TODO: Change '*' to be your username
        output2 = re.sub(r"tunnel-mtu=1500", "tunnel-mtu=1500\nusername=*", output)
        output3 = re.sub(r"service-type=org.freedesktop.NetworkManager.openvpn", r"service-type=org."
                                                                                 r"freedesktop.NetworkManager."
                                                                                 r"openvpn\n\n[vpn-secrets]\n"
                                                                                 # TODO: Change '*' to your VPN password
                                                                                 r"password=*\n",
                         output2)
        output4 = re.sub(r"password-flags=1", r"", output3)

        # Indents are important, m'kay? Without them, you do things like overwrite your wireless config files.
        # Take it from me: You do NOT want to do that.
        opened_file.truncate(0)
        opened_file.seek(0)
        opened_file.write(output4)
        opened_file.close()
        print(ovpn)

Now that the files are modified, we need to restart NetworkManager to allow the configurations to be re-read on the connection’s instantiation.

sudo service network-manager restart

(Personally, bouncing the machine was more of a formidable option, here, but that’s because I messed-up the initial script – due to indenting – and wiped all of my configuration files; thus, the warning in the script.)

Now that this has been configured, I needed a way to randomly choose which VPN connection to use, so as to not always land on a static connection. To do this, I used Python again and randomised the choice of which VPN server to connect to. (You’ll note that I’m using a pretty large seed and that’s because the default random.random() method isn’t random in a secure manner [read: it’s predictable].)

#!/usr/bin/env python
"""Randomly connections to a random VPN profile (if any are found).

Uses NetworkManager (https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html#) to
enumerate the devices and connections. First, we enumerate the wireless devices to ensure that we have one. Next, we
enumerate the VPN connections and put them into a list. After that, we randomly select one of the VPN connections to
connect to. Once we've accomplished this, we disconnect the current VPN connection (if one is found) and connect to
the randomly chosen VPN connection.

REQUIREMENTS:
    python-networkmanager

TO RUN:
    python NetworkManager_VPN.py
"""

__author__ = "felsokning"
__copyright__ = "Copyright 2019"
__license__ = "MIT"

import os
import random
import time
# Externals
import NetworkManager

# Find all of the VPN connections on the machine.
vpns = list()
current_vpn = None
wireless_device = None
connections = NetworkManager.Settings.ListConnections()
for c in connections:
    if "vpn" in c.GetSettings()['connection']['type']:
        path = c.object_path
        vpns.append(path)

# If there are no VPN connections, there's no point in proceeding.
if vpns.__len__() > 0:
    # We find the Wireless Network Interface.
    # If you're running some kind of weird, three wireless network cards situation, then...
    # Change this code to work in your use-case scenario.
    devices = NetworkManager.Device.all()
    for d in devices:
        if "wifi" in d.Driver:
            wireless_device = d

    # Validate that we found a wireless network interface
    if wireless_device is not None:
        # Get the currently active VPN connection.
        # If you're running some kind of weird, three active VPNs scenario, then...
        # Change this code to work in your use-case.
        active = NetworkManager.NetworkManager.ActiveConnections
        for a in active:
            if "vpn" in a.Type:
                current_vpn = a

        # Choose a random one to connect to. We use the far more secure random method, with a larger seed,
        # to try and prevent the random generation from being a predictable pattern (well, to try to make
        # it far less predictable with our sample, at least).
        rand = random.SystemRandom(os.urandom(99999999))
        random_int = rand.randint(0, (vpns.__len__() - 1))
        random_vpn = vpns.__getitem__(random_int)
        new_connection = NetworkManager.Connection(random_vpn)

        # Validate that we have a current VPN connection to disconnect from before we do.
        if current_vpn is not None:

            # Disconnect the old & busted.
            NetworkManager.NetworkManager.DeactivateConnection(current_vpn)

            # To prevent collision in NetworkManager, we allow background clean-up before reconnecting.
            time.sleep(10)

        # Connect the new hotness.
        print("Connecting to: {}".format(random_vpn))
        NetworkManager.NetworkManager.ActivateConnection(new_connection, wireless_device, "/")

    # No wireless interfaces were found, so let's abort.
    else:
        raise Exception("No wireless interfaces were found.")

# No VPN connections were found, so let's abort.
else:
    raise Exception("The hull has been breached and the science is leaking out! "
                    "(We didn't find any valid VPN connections on this machine via NetworkManager.)")

That should just about do it. …but WAIT! That’s not all! If you act now…

I’ve also written a script to remove all of the OVPN configurations from NetworkManager, in case you made a mistake somewhere (I know that I did and this came in pretty useful.)

#!/usr/bin/env python
"""Removes all VPN profiles found in NetworkManager.

Uses NetworkManager (https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html#) to
enumerate the connections that are specifically VPN connections. Then, we delete them, with righteous retribution and
indignation.

REQUIREMENTS:
    python-networkmanager

TO RUN:
    python3 RemoveVPNs.py
"""

__author__ = "felsokning"
__copyright__ = "Copyright 2019"
__license__ = "MIT"

import NetworkManager

# Find all of the VPN connections on the machine.
vpn_list = list()
connections = NetworkManager.Settings.ListConnections()
for c in connections:
    if "vpn" in c.GetSettings()['connection']['type']:
        vpn_list.append(c)

# We make sure we'e not spinning our wheels and/or calling delete on null.
if len(vpn_list) > 0:
    for v in vpn_list:
        print("Deleting: {}".format(v.object_path))
        v.Delete()

Now, you should have of the tools you need to mass-configure OVPN on your *nix (debian-based, probably) machine. 🙂

Happy coding!

Lessons in X-Language Implementations of the Same Action

It is, by no small irony, that it’s been some time that I’ve written a post. A lot’s been going on – including, but not limited to, travelling betwixt Ireland and Sweden. As a result of that, I haven’t – quite – had the free time nor desire to really sit down and dedicate time solely to creating a post but, after my recent ventures in programming for the same resultant set in multiple languages, I have a bit more of a reason and desire to make one.

I should preface that the idea came from multiple facets:

Sweden organises calendar years by weeks, which correlates with the ISO standard. You can see this evidenced by restaurant menus in Sweden.

Someone made a website that you can refer to, to look at what the week number is.

My friend/previous manager in Sweden suggested the idea, since I was already working on creating a variation of the week number site in Docker/Python.

The Idea

Since week numbers are a prominent feature, it would be worthwhile to write reusable code, which could be imported, to save others the time of having to write it – themselves.

Implementation

The first problem was that the implementation, to achieve the same desired net-effect result, would – obviously – be different between languages. So, for example, C# has the premise of extension methods but Rust requires Trait and Implementation to extend a given type.

To do this, it required a lot of research, trials, and much errors but, first, the pseudo-code needed to be defined so that I had a blueprint to use across the languages.

if($obj -eq date from datetime){obtain week number from the object via method}

With the pseudo-code defined, it was time for the implementation via code. Most of the development occurred on an Ubuntu 18.04LTS machine – save for the C#/C++ code, which occurred on a Windows 10 machine.

I’m only going to show two of the four implementations, here, so as to save on space.

C#

/// <summary>
///     Extends the System.DateTime class to include a method to return the week number.
/// </summary>
/// <param name="dateTime">The System.DateTime object to process.</param>
/// <returns>An integer signifying the current week number of the year.</returns>
public static int Veckan(this DateTime dateTime)
{
    // Jag behöver att säga tack till Peter Saverman för denna idé.
    Calendar calendar = CultureInfo.InvariantCulture.Calendar;
    DayOfWeek dayOfWeek = calendar.GetDayOfWeek(dateTime);
    if (dayOfWeek >= DayOfWeek.Monday && dayOfWeek <= DayOfWeek.Wednesday)
    {
        dateTime = dateTime.AddDays(3);
    }

    // Vi behöver att använda måndag för den första dagen på veckan
    // Se: https://en.wikipedia.org/wiki/ISO_week_date#Calculating_the_week_number_of_a_given_date
    return calendar.GetWeekOfYear(dateTime, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}

Python

from datetime import date


# Vi har att skapa en ny klass eftersom python har inte "extensions methods"
class SwedishDate(date):
    # Vi skulle heter det på svenska, nej?
    def veckan(self):
        return self.isocalendar()[1]

Lessons

So, the lessons learned weren’t very numerous but I suppose that the most prevalent is that documentation is quite worthwhile but it can oft be extremely difficult to find. Even if you find the documentation, it isn’t going to be as implicitly straightforward as one might think. For example, defining the return-type in Rust is vastly different than any other language that I’ve ever programmed in.

    fn veckan(&self) -> u32
    {
        let now = self.date().iso_week().week();
        return now;
    }

As you can see, the return-type (u32) is strongly-typed and appears after the initial function (read: method) definition and is always on the right of an arrow.

Also, while languages might seem different, the conceps behind their implementations have the same underlying concepts. Take the concept of ‘this‘ in C# and of ‘self‘ in Python. While, syntatically, they’re different in definition and use, their end-goal is effectively the same thing: Effect a specific instance of the type, which – at runtime – is the instance in question (well, any instance, in scope really but I’m cluttering this with abstraction).

Conclusion

Programming the same action across multiple languages causes us to learn more about those languages (it was my second program in Rust) and, while it can be a pain, the net result could be that it’s more of a benefit to the overall open-source community.