Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / PowerShell

VPNScripter: A Scripter for Windows' VPN Connections

Rate me:
Please Sign up or sign in to vote.
4.84/5 (16 votes)
21 Nov 2023MIT8 min read 59.9K   1.5K   31   10
Create quickly VPN connections in Windows according to a XML configuration file

Introduction

This is a general PowerShell script which creates automatically a set of VPN connections according to a XML configuration file. Very useful if you want to quickly configure VPN directly in Windows without using any custom VPN software developed by your VPN provider.

Background

VPNs have always been historically a way to connect to companies' private network from Internet. Nowadays, they have gone commercial being used mainly for privacy issues, routing all customer's internet traffic to relay servers distributed across the world. There are a few different protocols for VPN connections and Windows supports natively most of them, in particular:

  • PPTP: One of the first VPN protocols, based on the encapsulation (through GRE protocol) and encryption (RC4) of the PPP protocol [1]. PPTP was developed by Microsoft and it is now being abandoned due to the weak and insecure encryption algorithm. Client authentication is performed usually through a login and password, even though client certificates can be used. From a network viewpoint, PPTP uses TCP port 1723 and requires routers to forward IP protocol GRE.
  • L2TP/IPSEC: L2TP is a tunnelling protocol which like PPTP is used to encapsulate PPP protocol. Since L2TP is insecure (it does not have any kind of encryption), in order to provide security, it is often used together with another protocol, IPSEC [2]. L2TP client authentication requires a login and password and moreover client and server share usually also a PSK in order to allow the client to establish the IPSEC tunnel, even though the client can also authenticate to IPSEC server by certificates. From the network viewpoint, L2TP uses port UDP 1701, while IPSEC uses UDP ports 4500 and 500 (if not behind a NAT ip protocol ESP must also be forwarded by routers). It is the "bulkiest" protocol.
  • SSTP: Developed by Microsoft, like PPTP and L2TP, it is based on the encapsulation of PPP protocol using instead a SSL/TLS channel to secure all traffic (the same protocols used while browsing with https on secure internet websites). Like PPTP, client authentication is performed usually through a login and password, even though client certificates can be used. Since it uses only the standard HTTPS port (TCP 443), it is the most suitable protocol to bypass almost any firewall restriction and can also be used flawlessly with proxies.
  • IKEv2: It is a slight variation of IPSEC in which the client authentication is not done only by a PSK or a certificate, but also by using standard user-password credentials. Moreover, IKEv2 is much more resilient to changing network connectivity, making it a good choice for mobile users who move between access points. From a network viewpoint, IKEv2 uses UDP ports 500 and 4500 (if not behind a NAT ip protocol ESP must also be forwarded by routers).

Windows has also the automatic protocol which essentially attempts to establish the vpn connection by trying all protocols IKEv2, SSTP, L2TP/IPSEC, PPTP in order. For more information, please see [3].

Using the Code

All scripter configuration takes place in the XML file called vpnconfig.xml (see below). This file defines a set of VPN providers (Provider element node) and for each provider specifies a set of servers (Server sub element node) for each of which a VPN connection will be created in Windows. Let's examine the nodes:

  • Provider: This node corresponds to a VPN provider. Attributes:
    • name: Used to generate the VPN connection name in Windows
    • basedomain: Used to generate host name of the VPN server (usually a VPN provider offers a set of servers which share a common domain name). It can be overridden in server node.
    • l2tppsk: It's the pre-shared secret for L2TP/IPSEC connection (usually it is the same across all provider's servers).
    • user: It's the username to access the VPN (usually, it is the same across all provider's servers).
    • password: It's the password associated with the username (usually, it is the same across all provider's servers).
    • proto: Specifies the VPN protocol to use for all servers, if left empty, it means automatic.
  • Server: This node corresponds to a server of a VPN provider. Attributes:
    • server: Specifies the host name of the server. If this attribute does not contain the "." character, the host name is obtained by concatenating this field to provider's base domain, otherwise host name is equal to this field.
    • proto: Specifies the VPN protocol to use. It also overrides any protocol defined in provider node.
    • user: It's the username to access the VPN. It also overrides any user defined in provider node.
    • password: It's the password associated to the username. It also overrides any password defined in provider node.
XML
<!-- VpnScripter configuration file © 2016 Federico Di Marco

Provider defines the vpn providers you have. 
Each vpn provider has usually a set of servers to which you
can connect and the script creates a vpn connection 
for every server you specify for every provider listed.
The created connection will have:
- Name: Server attribute (till first .) + Provider name 
if proto is auto or empty, Server + Proto + Provider name if not null.        
- Protocol: the one specified in the server element or if null 
the one specified in the provider element or if null auto
- L2tp PSK, User, Password: the one specified in the server 
element or if null the one specified in the provider element 
(you don't have to repeat them for all servers)
- Server hostname: Server attribute + Provider base domain 
if server attributes does not contain any "." char
                  otherwise server attribute
-->

<Providers>
  <Provider name="TestVPN1" 
  basedomain="myvpn.com" user="user01" 
  password="hottie">   
    <Server server="sw" />   
    <!--Vpn will have sw.myvpn.com as host address and 
    SW TestVPN1 as name, automatic protocol-->
    <Server server="ro" proto="PPTP"/>   
    <!--Server proto overrides Provider specified protocol (auto in this case)-->
    <Server server="kick-vm.myvpnext.com" 
    proto="SSTP"/>  
    <!--Vpn will have kick-vm.myvpnext.com as host address 
    and KICK-VM TestVPN1 as name -->
    <Server server="sp" proto="IKEV2" 
    user="user15" password="beer" /> 
</Provider>
<Provider name="TestVPN2" basedomain="myvpn2.com" 
l2tppsk="12345" user="test001" password="master">
    <Server server="karate" proto="L2TP" 
    l2tppsk="314pi"/> 
    <Server server="kazu"  /> 
    <Server server="moon"  /> 
    <Server server="sun" /> 
</Provider>
</Providers>

Inside the zip archive, you will find 3 files:

  • DotRas.dll: It's a .NET class library which is used by the PowerShell script to create VPN connections and it must be in the same folder of the script.
  • vpnconfig.xml: It's the above XML configuration file which must be edited with your settings.
  • VpnScripter.ps1: It is a PowerShell script which can be launched from PowerShell or double clicked. It accepts an optional parameter named ConfigFile specifying the name of the XML configuration file to use (defaults to vpnconfig.xml). You can specify the -Debug flag to obtain a more verbose log output.

The script has been tested on Windows 10, but should also work in any Windows with at least PowerShell 3.0.

Implementation Overview

Basically, all the code is inside a single PowerShell file called VpnScripter.ps1, which contains a mixture of XSD, C# and PowerShell code. Basically it:

  • Parses and verifies the XML configuration file vpnconfig.xml using an embedded XSD schema defined inside the PowerShell script.
    The schema itself is very simple (e.g. contains mainly xs:attribute clauses specifying the allowed/required attributes of the configuration file) and defines a simple type called NotEmptyTrimmedString which, as the name implies, checks by a regular expression that an attribute value is a "not empty trimmed string" (it is used for server attribute which must be "meaningful"). The verification of XSD schema is performed through a PowerShell function ValidateLoadXml which uses standard .NET functions to perform validation (e.g., C# new XmlDocument().Load(XmlReader.Create(<file>,<settings with schema file>)))
    C#
    function ValidateLoadXml([string] $XmlFile, [string] $Schema) {
    
    	$verr={ 
    		Write-Error "Error: malformed XSD/XML Line: $($_.Exception.LineNumber) 
    		Offset: $($_.Exception.LinePosition) - $($_.Message)" 
    		throw [System.IO.InvalidDataException] 
    	}
    
    	try {
    		[System.Xml.XmlReaderSettings]$readsett=New-Object System.Xml.XmlReaderSettings
    		$readsett.Schemas.Add([System.Xml.Schema.XmlSchema]::Read
    		((New-Object System.IO.StringReader($Schema)),$verr))
    		$readsett.ValidationType=[System.Xml.ValidationType]::Schema
    		$readsett.add_ValidationEventHandler($verr)
    		$xmlconf = New-Object System.Xml.XmlDocument
    		$xmlconf.Load([System.Xml.XmlReader]::Create($XmlFile,$readsett))
    	}
    	catch [System.IO.InvalidDataException]  {
    		return $null
    	}
    	
    	return $xmlconf
    }	
  • Cycles for all Server nodes of all the Provider nodes of the XML configuration file
    • Builds the list of parameters needed to create the VPN entry in Windows according to aforementioned rules (if server attribute contains a point '.', it's the full DNS name of the VPN endpoint, otherwise it must be concatenated with the basedomain attribute, etc.). In order to evaluate the needed user, password, protocol and l2tppsk from the information contained in Server and Provider nodes, the script uses a coalescing operator, quite common in SQL queries and C# (operator ??), but unluckily missing in PowerShell, hence implemented in custom defined PowerShell function called Coalesce.
      PowerShell
      function Coalesce([string[]] $StringsToLookThrough, [switch]$EmptyStringAsNull) {
        if ($EmptyStringAsNull.IsPresent) {
          return ($StringsToLookThrough | Where-Object { $_ } | Select-Object -first 1)
        }
        else {
          return (($StringsToLookThrough -ne $null) | Select-Object -first 1)
        }  
      }	
    • Performs additional checks on parameters which are not so straightforward to do with XSD:
      • If proto attribute is L2TP, the l2tppsk attribute must be not empty
      • The user and password attributes must not be empty
    • Checks if the VPN entry already exists in Windows through Get-VpnConnection cmdlet and if so deletes it through Remove-VpnConnection cmdlet.
      PowerShell
      $exist=Get-VpnConnection -Name $vpnname -ErrorAction silentlycontinue
      if ($exist -ne $null) {
          Write-Host "Info: Removing VPN connection $vpnname"
      
          Remove-VpnConnection -Name $vpnname -Force
      }
      
    • Creates a new VPN entry by calling a custom defined C# function called Add.
      Basically, this function sets up the VPN parameters (proto is converted into an enum by a simple conversion function called ConvertProto, with an IKEV2 VPN EAP authentication must be used, etc.) and straightforwardly calls the DotRas.dll functions to add a new VPN entry to Windows.
      C#
      public static void Add(string path,string name,string server,
      string proto, string l2tppsk, string user, string password) {
          RasPhoneBook PhoneBook=new RasPhoneBook();
          PhoneBook.Open(path);
          RasEntry VpnEntry = RasEntry.CreateVpnEntry
          (name,server, ConvertProto(proto),
          RasDevice.Create(name, DotRas.RasDeviceType.Vpn), true);
          VpnEntry.Options.UsePreSharedKey = true;
          VpnEntry.Options.CacheCredentials = true;
          VpnEntry.Options.ReconnectIfDropped = true;
          if (VpnEntry.VpnStrategy==RasVpnStrategy.IkeV2Only) {
              // 23 EAP-AKA
              // 50 EAP-AKA'
              // 18 EAP-SIM
              // 21 EAP-TTLS
              // 25 PEAP
              // 26 EAP-MSCHAPV2
              // 13 EAP-smart card or certificate
              VpnEntry.Options.RequireEap = true;
              VpnEntry.CustomAuthKey=26; // 26 means eap-mschapv2 username/password
          }
          else {
              VpnEntry.Options.RequireMSChap2 = true;
          }
          //VpnEntry.Options.RequireWin95MSChap = false; // seems to be ignored,
          //chap is still checked in newly created vpn profile
          //VpnEntry.Options.RequireMSChap = false;  // seems to be ignored,
          //chap is still checked in newly created vpn profile
          //VpnEntry.Options.RequireChap = false; // seems to be ignored,
          //chap is still checked in newly created vpn profile
          VpnEntry.EncryptionType = RasEncryptionType.RequireMax;
          PhoneBook.Entries.Add(VpnEntry);
          VpnEntry.UpdateCredentials(RasPreSharedKey.Client,l2tppsk);
          VpnEntry.UpdateCredentials(new System.Net.NetworkCredential(user,password));
      }
      

As already said, both the XSD schema and the .NET helper code have been embedded inside the PowerShell script in a single, almost self-contained (needs DotRas.dll) file. In fact, PowerShell scripts have the nice feature to allow the inclusion of any .NET code inside allowing them to do everything that can be done with C# code. It's interesting the way in which a C# code can be parsed and compiled:

  • First "load" any DLL (with Add-Type -Path <dll file>) or assembly (with Add-Type -AssemblyName <assembly>) used by the custom code
  • Then parse and compile your custom C# code with
    Add-Type -ReferencedAssemblies <list of referenced assemblies> -TypeDefinition <string containing C# code> -Language CSharp
PowerShell
Add-Type -Path $psscriptroot\DotRas.dll
Add-Type -AssemblyName System.Xml
Add-Type -ReferencedAssemblies $psscriptroot\DotRas.dll,System.Xml 
         -TypeDefinition $Source -Language CSharp

Points of Interest

The external library DotRas.dll has been used because there does not seem to be a way to configure credentials (login and password) with the standard PowerShell command Add-VpnConnection, please let me know if you find a way.

Repository

You can find the source code repository on GitHub. If you want to add a predefined configuration file for your VPN provider with all servers or to improve the code, you are welcome.

History

  • V1.0 (2nd December 2016
    • First release
  • V1.1. (7th December 2016)
    • Added override of l2tppsk, user and password in server nodes
    • Added XML validation through XSD and simple checks on user/password/l2tppsk parameters
  • V1.2 (27th March 2017)
    • Added "Implementation overview" section

References

  • [1] PPP is an old point-to-point protocol historically used to connect to ISP by modems. It allows to establish a connection between two endpoints.
  • [2] IPSEC is an encrypted tunnel between two endpoints based on a set of rules (called security associations) establishing the encryption and integrity algorithms to be used in the tunnel and the network packets which need to be tunnelled (e.g. all TCP packets, TCP addressed to a particular port, etc.). Basically, it encapsulates TCP/UDP in a new encrypted IP packet (ESP). The algorithms used to encrypt and to check message's integrity are established through a negotiation protocol called IKE. Since client and server mainly authenticate mutually by certificates, IPSEC is usually used in lan-to-lan VPN (e.g. connecting two different organizations' network) and not in a client-to-lan scenario, however for this kind of scenario, it is possible to authenticate through a PSK (pre-shared key or shared secret), although this entails a lower security.
  • [3] Microsoft VPN Tunnelling Protocols

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior)
Italy Italy
I'm a senior .NET software engineer in the second half of my forties. Started using a computer at six years, gone through logo, basic, assembly, C/C++, java and finally to .NET and .NET core. Proficient also in databases, especially Sql Server and reporting. Let's say I have also some experience on security but mainly in the past, now things have become much more difficult and I do not have too much time to keep me updated, but sometimes I am still kicking in. Fan of videogames, technologies, motorbikes, travelling and comedy.

Email: Federico Di Marco <fededim@gmail.com>
Linkedin: LinkedIn
Github: GitHub
Stackoverflow: StackOverflow

Comments and Discussions

 
QuestionGetting a DotNetMethodException error Pin
Member 1354399927-Nov-17 9:54
Member 1354399927-Nov-17 9:54 
BugCan not connect by rasdial Pin
Member 134248078-Nov-17 23:05
Member 134248078-Nov-17 23:05 
QuestionPowershell script for setting VPN username and password without the need for a logon script. Pin
stano29-Aug-17 0:05
stano29-Aug-17 0:05 
I know this is an old post, but I hit it trying to solve a problem of setting the username and password for a VPN connection after setting it up in Powershell.

I've created a module VPNCredentialsHelper that can set the username and password directly for the connection so that you are not prompted to enter it the first time you connect.

Install-Module -Name VPNCredentialsHelper

And then you can do something like this:

$name = "ExpressVPN Australia Sydney"
$address = "aus1-ubuntu-l2tp.expressprovider.com"
$username = "your_username"
$plainpassword = "your_password"

Add-VpnConnection -Name $name -ServerAddress $address -TunnelType L2tp -EncryptionLevel Required -AuthenticationMethod MSChapv2 -L2tpPsk "12345678" -Force:$true -RememberCredential:$true -SplitTunneling:$false 

Set-VpnConnectionUsernamePassword -connectionname $name -username $username -password $plainpassword -domain ''



For those of you more code minded, here is the underlying C# code. A huge thanks to Jeff Winn for the DotRas project (https://dotras.codeplex.com/) which showed me the way, and who did all the really hard work.

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp6
{
    public class VPNHelper
    {
        private const int SUCCESS = 0;
        private const int ERROR_ACCESS_DENIED = 5;

        private const int UNLEN = 256;// Defines the maximum length of a username.
        private const int PWLEN = 256;// Defines the maximum length of a password.
        private const int DNLEN = 15;// Defines the maximum length of a domain name.

        [Flags]
        private enum RASCM
        {
            None = 0x0,
            UserName = 0x1,
            Password = 0x2,
            Domain = 0x4
        }

        [DllImport("rasapi32.dll", CharSet = CharSet.Unicode)]
        private static extern int RasGetErrorString(
            int uErrorValue,
            [In, Out] string lpszErrorString,
            int cBufSize);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)]
        private struct RASCREDENTIALS
        {
            public int size;
            public RASCM options;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = UNLEN + 1)]
            public string userName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = PWLEN + 1)]
            public string password;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = DNLEN + 1)]
            public string domain;
        }

        [DllImport("rasapi32.dll", CharSet = CharSet.Unicode)]
        private static extern int RasSetCredentials(
            string lpszPhonebook,
            string lpszEntryName,
            IntPtr lpCredentials,
            [MarshalAs(UnmanagedType.Bool)] bool fClearCredentials);

        public static bool SetCredentials(string entryName, string domain, string username, string password)
        {
            var credentials = new RASCREDENTIALS() { userName = username, password = password, domain = domain ?? string.Empty, options = RASCM.Domain | RASCM.UserName | RASCM.Password };

            int size = Marshal.SizeOf(typeof(RASCREDENTIALS));

            IntPtr pCredentials = IntPtr.Zero;
            try
            {
                credentials.size = size;

                pCredentials = Marshal.AllocHGlobal(size);
                Marshal.StructureToPtr(credentials, pCredentials, true);

                int ret = RasSetCredentials(null, entryName, pCredentials, false);

                switch (ret)
                {
                    case SUCCESS:
                        return true;
                    case ERROR_ACCESS_DENIED:
                        throw new UnauthorizedAccessException();
                    default:
                        throw ProcessRASException(ret);
                }
            }
            finally
            {
                if (pCredentials != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(pCredentials);
                }
            }
        }

        private static Exception ProcessRASException(int errorCode)
        {
            try
            {
                string buffer = new string('\x00', 512);

                int ret = RasGetErrorString(errorCode, buffer, buffer.Length);
                if (ret == SUCCESS)
                    return new RASException(errorCode, buffer.Substring(0, buffer.IndexOf('\x00')));
            }
            catch (EntryPointNotFoundException)
            {
            }

            return new RASException(errorCode, "RAS Error code: " + errorCode.ToString());
        }

        public class RASException: Exception
        {
            public RASException(int errCode, string message):base(message)
            {
                RASErrorCode = errCode;
            }

            public int RASErrorCode { get; private set; }
        }
    }
}

http://ximura.blogspot.com/

AnswerRe: Powershell script for setting VPN username and password without the need for a logon script. Pin
Federico Di Marco21-Nov-23 11:36
Federico Di Marco21-Nov-23 11:36 
QuestionHave you consider to post this as a tip? Pin
Nelek7-Dec-16 1:39
protectorNelek7-Dec-16 1:39 
AnswerRe: Have you consider to post this as a tip? Pin
Federico Di Marco8-Dec-16 21:50
Federico Di Marco8-Dec-16 21:50 
GeneralRe: Have you consider to post this as a tip? Pin
Nelek9-Dec-16 0:01
protectorNelek9-Dec-16 0:01 
GeneralRe: Have you consider to post this as a tip? Pin
Federico Di Marco28-Dec-16 12:02
Federico Di Marco28-Dec-16 12:02 
GeneralRe: Have you consider to post this as a tip? Pin
Nelek25-Mar-17 10:53
protectorNelek25-Mar-17 10:53 
GeneralRe: Have you consider to post this as a tip? Pin
Federico Di Marco27-Mar-17 5:57
Federico Di Marco27-Mar-17 5:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.