Skip to content

Instantly share code, notes, and snippets.

@Diyagi
Last active January 16, 2025 16:49
Show Gist options
  • Save Diyagi/6aeb10940e3eaa903be2c521897a5aac to your computer and use it in GitHub Desktop.
Save Diyagi/6aeb10940e3eaa903be2c521897a5aac to your computer and use it in GitHub Desktop.
Powershell implementation of NAT-PMP made for use with ProtonVPN
function Request-NATPMPPort {
param (
[int]$PublicPort = 1,
[int]$PrivatePort = 0,
[string]$Protocol = "udp",
[string]$GatewayIP = "10.2.0.1",
[int]$Port = 5351,
[int]$Lifetime = 60
)
# UInt16 LSB Number -> Int16 MSB Byte Array
function PortToInt16MSBBytes {
param (
[int]$Port
)
if ($Port -gt [UInt16]::MaxValue -or $Port -lt [UInt16]::MinValue) {
Write-Error "Port range is invalid."
exit
}
$Int16LSBPort = [BitConverter]::ToInt16([BitConverter]::GetBytes([UInt16]$Port), 0)
return [BitConverter]::GetBytes([System.Net.IPAddress]::HostToNetworkOrder($Int16LSBPort))
}
# Int16 MSB Byte Array -> UInt16 LSB Number
function BytesToUInt16LSBPort {
param (
[array]$PortBytes
)
$Int16LSBPort = [System.Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt16($PortBytes, 0))
return [BitConverter]::ToUInt16([System.BitConverter]::GetBytes($Int16LSBPort), 0)
}
if ($Protocol.ToLower() -eq "tcp") {
$protocolCode = 0x01
}
elseif ($Protocol.ToLower() -eq "udp") {
$protocolCode = 0x02
}
else {
Write-Error "Invalid protocol, accepted values are UDP or TCP."
}
# NAT-PMP message to request a random public UDP port
$version = 0x00 # Version
$opCode = $protocolCode # Operation code for port mapping request
$reserved = 0x00, 0x00 # Reserved byte
$privatePortBytes = PortToInt16MSBBytes($PrivatePort)
$publicPortBytes = PortToInt16MSBBytes($PublicPort)
$lifetimeBytes = [BitConverter]::GetBytes([System.Net.IPAddress]::HostToNetworkOrder([BitConverter]::ToInt32([BitConverter]::GetBytes([UInt32]$Lifetime), 0)))
$request = @($version, $opCode) + $reserved + $privatePortBytes + $publicPortBytes + $lifetimeBytes
$requestBytes = [byte[]]@($request | ForEach-Object { $_ -as [byte] })
$udpClient = New-Object System.Net.Sockets.UdpClient(0)
$udpClient.client.ReceiveTimeout = 1000
$udpClient.Connect($GatewayIP, $Port)
[void]$udpClient.Send($requestBytes, $requestBytes.length)
try {
$udp_endpoint = New-Object System.Net.IPEndPoint([system.net.ipaddress]::Any, 0)
$responseBytes = $udpClient.Receive([ref] $udp_endpoint)
if ($responseBytes.Length -ge 16 -and $responseBytes[1] -eq $protocolCode + 0x80) {
$mappedPrivatePort = BytesToUInt16LSBPort($responseBytes[8..9])
$mappedPublicPort = BytesToUInt16LSBPort($responseBytes[10..11])
$lifetime = [System.Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($responseBytes, 12))
Write-Output "Mapped public port $mappedPublicPort protocol $($Protocol.ToUpper()) to local port $mappedPrivatePort lifetime $lifetime"
#Write-Host ($responseBytes | ForEach-Object { "{0:X2}" -f $_ })
}
else {
Write-Error "Failed to map port. Response: $([System.BitConverter]::ToString($responseBytes))"
return $null
}
}
catch {
Write-Error "Timeout"
}
finally {
$udpClient.Close()
}
}
while ($true) {
Request-NATPMPPort -PublicPort 1 -PrivatePort 0 -Protocol "UDP" -GatewayIP "10.2.0.1" -Lifetime 60
Request-NATPMPPort -PublicPort 1 -PrivatePort 0 -Protocol "TCP" -GatewayIP "10.2.0.1" -Lifetime 60
Start-Sleep -Seconds 45
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment