OpenVPNClient.psm1
<#
.SYNOPSIS Generates a Google Authenticator Token .DESCRIPTION Takes in a BASE32 encoded $Secret and generates an object with string Pin and int SecondsRemaining parameters .PARAMETER Secret BASE32 encoded Secret e.g. 5WYYADYB5DK2BIOV .EXAMPLE $token = Get-GoogleAuthenticatorPin -Secret 5WYYADYB5DK2BIOV Write-Host "Token's PIN is" $token.Pin "with" $token.SecondsRemaining "seconds remaining" .NOTES Uses Otp.NET https://github.com/kspearrin/Otp.NET #> function Get-GoogleAuthenticatorPin{ [CmdletBinding()] [OutputType([OpenVPNClient.GoogleAuthenticatorPin])] [Alias("ggap")] param ( [Parameter(Mandatory=$true, Position=0, HelpMessage="BASE32 encoded Secret e.g. 5WYYADYB5DK2BIOV")] [String] $Secret ) return [OpenVPNClient.GoogleAuthenticatorPin]::Get($Secret) } <# .SYNOPSIS Initates an instance of the openvpn process .DESCRIPTION Uses OpenVPNInteractiveService or admin permission to invoke a new openvpn process .PARAMETER WorkingDirectory Working directory for OpenVPN process, defaults to current directory .PARAMETER OpenVPNOptions Command line arguments to pass to OpenVPN .PARAMETER StdIn Input to send into started OpenVPN process. The LF (U000A) character can be used to simulate an enter key. .EXAMPLE An example .NOTES See https://community.openvpn.net/openvpn/wiki/OpenVPNInteractiveService Also the OpenVPNClient.*.dll may be used .NET #> function Start-OpenVPN{ [CmdletBinding()] [OutputType([int])] [Alias("sovpn")] param ( [Parameter(Mandatory=$false,HelpMessage="Working directory for OpenVPN process, defaults to current directory")] [String] $WorkingDirectory = "", [Parameter(Mandatory=$false,HelpMessage="Command line arguments to pass to OpenVPN")] [String] $OpenVPNOptions = "", [Parameter(Mandatory=$false,HelpMessage="Input to send into started OpenVPN process")] [String] $StdIn = "" ) if ([string]::IsNullOrEmpty($WorkingDirectory)) { $WorkingDirectory = [System.IO.Directory]::GetCurrentDirectory(); Write-Debug -Message "Using $WorkingDirectory as WorkingDirectory since none is provided" } return [OpenVpnClient.Process]::Start($WorkingDirectory, $OpenVPNOptions, $StdIn).GetAwaiter().GetResult() } <# .SYNOPSIS Adds Windows permissions to file .DESCRIPTION Long description .PARAMETER SecretFile File path to secure .EXAMPLE An example .NOTES General notes #> function SecureFile{ [CmdletBinding()] param ( [Parameter()] [string] $SecretFile ) $sec = Get-Acl $SecretFile $sec.SetAccessRuleProtection($true, $false) $sec.RemoveAccessRuleAll((New-Object System.Security.AccessControl.FileSystemAccessRule("SYSTEM","FullControl","Allow"))) $sec.RemoveAccessRuleAll((New-Object System.Security.AccessControl.FileSystemAccessRule("Administrators","FullControl","Allow"))) $sec.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule([System.Security.Principal.WindowsIdentity]::GetCurrent().User,"FullControl","Allow"))) $sec | Set-Acl } function Connect-OpenVPN{ [CmdletBinding(DefaultParameterSetName="Direct")] [OutputType([int])] [Alias("covpn")] param( [Parameter(Mandatory=$true,ParameterSetName="Direct")] [Parameter(Mandatory=$true,ParameterSetName="Recur")] [System.IO.FileInfo] $Config, [Parameter(Mandatory=$false,ParameterSetName="Direct")] [pscredential] $Credential, [Parameter(Mandatory=$false,ParameterSetName="Recur")] [System.IO.FileInfo] $CredentialFile, [Parameter(Mandatory=$false,ParameterSetName="Recur")] [System.IO.FileInfo] $SecretFile, [Parameter(Mandatory=$false,ParameterSetName="Recur")] [switch] $IgnoreUserDomain ) if($PsCmdlet.ParameterSetName -eq "Direct"){ Write-Debug "Connecting OpenVPN with $Config for user $($Credential.UserName)" $openConnectionsLocation = New-Item -Path "$env:USERPROFILE/OpenVPN/OpenConnections" -ItemType Directory -Force $results = $openConnectionsLocation|Get-ChildItem -Directory|ForEach-Object{ $openConnectionItem = $_ $removePath = $false $pidpath ="$($openConnectionItem.FullName)/pid.txt" Write-Debug "Checking $($openConnectionItem.FullName)" if(Test-Path $pidpath) { $processid = [int]::Parse((Get-Content $pidpath -Raw)) Write-Debug "PID $processid detected at $pidpath" $proc = Get-Process -Id $processid -ErrorAction Ignore if(!$proc -or $proc.HasExited -or $proc.ProcessName -ne 'openvpn'){ Write-Debug "PID $processid is dead and should be removed" $removePath = $true }else{ Write-Debug "PID $processid is alive" $openConnectionItem | Get-ChildItem -File -Filter '*.ovpn'|ForEach-Object { Write-Debug "Checking OVPN $_" if($Config.Name -eq $_.Name){ Wait-OpenConnectionReady -OpenConnectionDirectory $openConnectionItem $processid } } } } else{ $removePath = $true } if($removePath){$openConnectionItem|Remove-Item -Recurse -Force} } if($results){ Write-Debug "In-progress connection detected $results" return $results }else{ $guid = New-Guid $newconfigpath = New-Item -Path "$($openConnectionsLocation.FullName)/$guid" -ItemType Directory -Force $newconfigfile = New-Item -Path "$($newconfigpath.FullName)/$($Config.Name)" -ItemType File -Force Write-Debug "Initializing connection $($newconfigpath.FullName)" $configcontent = Get-Content $Config -Raw if($Credential){ $configcontent = $configcontent.Replace("auth-user-pass", "auth-user-pass auth.$guid.txt") } Set-Content -Value $configcontent -Path $newconfigfile -NoNewline $options = "--config `"$newconfigfile`" --log `"$($newconfigpath.FullName)/out.log`" --writepid `"$($newconfigpath.FullName)/pid.txt`"" if($Credential){ Write-Debug "Attaching $($Credential.UserName) credential to $($newconfigpath.FullName)" $secretfile = New-Item -Path "$($newconfigpath.FullName)/auth.$guid.txt" -ItemType File -Force $username = $Credential.UserName $password = [System.Net.NetworkCredential]::new("", $Credential.Password).Password @($username,$password)|Set-Content $secretfile $result = Start-OpenVPN -WorkingDirectory $newconfigpath -OpenVPNOptions $options -StdIn "$username`n$password`n" }else{ Write-Debug "No credential for $newconfigpath" $result = Start-OpenVPN -WorkingDirectory $newconfigpath -OpenVPNOptions $options -StdIn "" } Wait-OpenConnectionReady -OpenConnectionDirectory $newconfigpath if(!$Credential){ $secretfile | Remove-Item -Force } Write-Debug "Started process $result" return $result } } elseif($PsCmdlet.ParameterSetName -eq "Recur"){ $askForCredentials = !$CredentialFile.Exists $connected = $false do{ Write-Debug "Attempting VPN connection" $rawcredential = if($askForCredentials){ $askmsg = "Enter Credential for $CredentialFile" if($username){ Get-Credential -UserName $username -Message $askmsg }else{ Get-Credential -Message $askmsg } $credentialInputted = $true }else{ Write-Debug "Importing credentials from $CredentialFile" Import-Clixml -Path $CredentialFile $credentialInputted = $false } if(!$rawcredential){ throw "No credential provided" } $username = $rawcredential.UserName $askForCredentials = $false $credential = $rawcredential if($SecretFile){ if($SecretFile.Exists){ Write-Debug "Importing secret from $SecretFile" $secret = Import-Clixml -Path $SecretFile }else{ $secret = Read-Host -AsSecureString -Prompt "Enter secret for $SecretFile" Export-Clixml -Path $SecretFile -InputObject $secret SecureFile -SecretFile $SecretFile Write-Verbose "Exported secret to $SecretFile" $SecretFile.Refresh() } Write-Debug "Attaching Google Token to $($credential.UserName)" $credential = $credential | Add-GoogleTokenToCredential -Secret $secret } if($IgnoreUserDomain -and $IgnoreUserDomain.IsPresent){ Write-Debug "Removing Domain from $($credential.UserName)" $credential = $credential | Remove-DomainFromCredential } try{ $result = Connect-OpenVPN -Config $Config -Credential $credential $connected = $true Write-Debug "Successfully connected OpenVPN with $Config for user $($credential.UserName)" if($credentialInputted){ Export-Clixml -Path $CredentialFile -InputObject $rawcredential SecureFile -SecretFile $CredentialFile Write-Verbose "Exported $($rawcredential.UserName) credential to $CredentialFile" $CredentialFile.Refresh() } }catch{ if($_.Exception.Message -eq "Invalid Username or Password (AUTH: Received control message: AUTH_FAILED)"){ Write-Warning $_ $askForCredentials = $true } else{ throw $_ } } }while(!$connected) return $result } } <# .SYNOPSIS Pauses until a directory being used by openvpn indicates connectivity .DESCRIPTION Pauses until certain key phrases inside the log file for a directory being used by openvpn indicates successful connection, or fatal error .PARAMETER OpenConnectionDirectory Directory containing out.log file associated with active openvpn connection .EXAMPLE An example .NOTES Not to be used without Connect-OpenVpn #> function Wait-OpenConnectionReady{ [CmdletBinding()] [OutputType([void])] param( [System.IO.DirectoryInfo] $OpenConnectionDirectory ) $result = "" do{ Write-Debug "Waiting for Connection $OpenConnectionDirectory" Start-Sleep -Milliseconds 2000 $file = $OpenConnectionDirectory|Get-ChildItem -File -Filter 'out.log' -ErrorAction Continue if($file){ Write-Debug "Copying $file for check" $file2 = $file|Copy-Item -Destination "$($OpenConnectionDirectory.FullName)/out.$(New-Guid).log" -Force -PassThru $content = Get-Content $file2 -Raw if($content.Contains("AUTH: Received control message: AUTH_FAILED")){ $result = "Invalid Username or Password (AUTH: Received control message: AUTH_FAILED)" } elseif($content.Contains("Initialization Sequence Completed With Errors ( see http://openvpn.net/faq.html#dhcpclientserv )")){ $result = "TCP/IP stack is corrupted (see http://openvpn.net/faq.html#dhcpclientserv)" } elseif($content.Contains("Initialization Sequence Completed")){ $result = "Success" } elseif($content.Contains("Exiting due to fatal error")){ $result = "Exiting due to fatal error : check $file for details" } $file2|Remove-Item } else{ Write-Debug "$file not found" } }while($result -eq "") if($result -ne "Success"){ throw $result } } function Add-GoogleTokenToCredential{ [CmdletBinding()] [OutputType([pscredential])] [Alias("agttc")] param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [pscredential] $Credential, [Parameter(Mandatory=$true)] [securestring] $Secret ) $textSecret = [System.Net.NetworkCredential]::new("", $Secret).Password $pin = Get-GoogleAuthenticatorPin -Secret $textSecret $newpw = $Credential.Password.Copy() $pin.Pin.ToCharArray()|ForEach-Object{$newpw.AppendChar($_)} return [pscredential]::new($Credential.UserName,$newpw) } function Remove-DomainFromCredential{ [CmdletBinding()] [OutputType([pscredential])] [Alias("rdfc")] param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [pscredential] $Credential ) if($Credential.UserName.Contains('\')){ $res = $Credential.UserName.Split('\') $res2 = [string[]]::new($res.Count-1) for($i = 1;$i -lt $res.Count;$i++){ $res2[$i-1] = $res[$i] } $resname = [string]::Join('\',$res2) } else{ Write-Debug "No Domain found in credential username $($Credential.UserName)" return $Credential } return [pscredential]::new($resname,$Credential.Password) } Export-ModuleMember -Function Get-GoogleAuthenticatorPin -Alias ggap Export-ModuleMember -Function Start-OpenVPN -Alias sovpn Export-ModuleMember -Function Connect-OpenVPN -Alias covpn Export-ModuleMember -Function Add-GoogleTokenToCredential -Alias agttc Export-ModuleMember -Function Remove-DomainFromCredential -Alias rdfc |