I'm trying to find a way to parse a user entered IP address information, e.g.:
192.168.100.10
192.168.100.10:80
21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2
21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A
[21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2]:8080
21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A:8080
www.microsoft.com
www.microsoft.com:80
The WinAPI function ParseNetworkString claims to be able to parse all these formats (the list came from the documentation). And a lot of them do indeed parse. But some fail:
192.168.100.10
✅192.168.100.10:80
✅21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2
✅21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A
✅[21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2]:8080
❌21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A:8080
❌www.microsoft.com
✅www.microsoft.com:80
✅So the pseudocode:
NET_ADDRESS_INFO^ addressInfo;
UInt16^ portNumber;
UInt8^ prefixLength;
DWORD res = ParseNetworkString(
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A:8080", //NetworkString
NET_STRING_IPV6_SERVICE_NO_SCOPE, //Types
addressInfo, portNumber, lengthPrefix);
comes back 87 The Parameter is incorrect..
What am i doing wrong?
In the same way the API says it can parse:
192.168.100.10:8080
\____________/ \__/
| |
Address Port
It also claims it can parse:
21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A:8080
\_____________________________________/ \__/
| |
Address Port
Some people might object, and claim that the only way to have a port number included in a canonical IPv6 address is if the address portion is enclosed in [square backets]. Those people are simply wrong. Source: RFC5952
In the end, what we want is to be able to pass what the user entered to WSAConnectByName, which takes:
nodeName
: e.g. "21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A"serviceName
e.g. "3119"# Service - where Port is mandatory
# ---------------------------------
# NET_STRING_ANY_SERVICE (where port is mandatory)
# NET_STRING_ANY_SERVICE_NO_SCOPE
# NET_STRING_NAMED_SERVICE (e.g. "www.microsoft.com:80")
# NET_STRING_IP_SERVICE
# NET_STRING_IP_SERVICE_NO_SCOPE
# NET_STRING_IPV4_SERVICE (e.g. "192.168.100.10:80")
# NET_STRING_IPV6_SERVICE_NO_SCOPE (e.g. "[21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A]:8080") scope is forbidden
# NET_STRING_IPV6_SERVICE (e.g. "[21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2]:8080") scope is optional
#
# Address - where port is forbidden
# ----------------------------------
# NET_STRING_ANY_ADDRESS
# NET_STRING_ANY_ADDRESS_NO_SCOPE
# NET_STRING_NAMED_ADDRESS (e.g. "www.microsoft.com")
# NET_STRING_IP_ADDRESS
# NET_STRING_IP_ADDRESS_NO_SCOPE
# NET_STRING_IPV4_ADDRESS (e.g. "192.168.100.10")
# NET_STRING_IPV6_ADDRESS_NO_SCOPE (e.g. "21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A") scope is forbidden
# NET_STRING_IPV6_ADDRESS (e.g. "21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2") scope is optinal
#
# Network - where CIDR is required (Scope and port forbidden)
# -----------------------------------------------------------
# NET_STRING_IP_NETWORK
# NET_STRING_IPV4_NETWORK (e.g. "192.168.100/24")
# NET_STRING_IPV6_NETWORK (e.g. "21DA:D3::/48")
import ctypes
from ctypes import POINTER, Structure, c_ulong, c_ushort, c_ubyte, c_wchar_p, windll, c_uint
from ipaddress import IPv6Address, IPv4Address
# Structures and constants for the WinAPI call
class NET_ADDRESS_INFO(Structure):
class _U(Structure):
_fields_ = [("Ipv4Address", ctypes.c_ubyte * 4),
("Ipv6Address", ctypes.c_ubyte * 16),
("DnsName", ctypes.c_wchar_p)]
_fields_ = [("Format", c_uint), ("U", _U)]
NET_ADDRESS_FORMAT_UNSPECIFIED = 0
NET_ADDRESS_DNS_NAME = 1
NET_ADDRESS_IPV4 = 2
NET_ADDRESS_IPV6 = 3
NET_STRING_ANY_SERVICE = 0x00000222
NET_STRING_ANY_ADDRESS = 0x00000209
# Load the DLL containing the ParseNetworkString function
iphlpapi = windll.Iphlpapi
# Define the function prototype
iphlpapi.ParseNetworkString.argtypes = [c_wchar_p, c_ulong, POINTER(NET_ADDRESS_INFO), POINTER(c_ushort), POINTER(c_ubyte)]
iphlpapi.ParseNetworkString.restype = c_ulong
# Function to call ParseNetworkString
def call_parse_network_string(network_string):
address_info = NET_ADDRESS_INFO()
port_number = c_ushort()
prefix_length = c_ubyte()
# First try parsing as a service
result = iphlpapi.ParseNetworkString(
network_string,
NET_STRING_ANY_SERVICE,
ctypes.byref(address_info),
ctypes.byref(port_number),
ctypes.byref(prefix_length)
)
# If failed, try parsing as an address
if result != 0:
result = iphlpapi.ParseNetworkString(
network_string,
NET_STRING_ANY_ADDRESS,
ctypes.byref(address_info),
ctypes.byref(port_number),
ctypes.byref(prefix_length)
)
if result != 0:
return f"Error: {ctypes.WinError(result).strerror}"
# Determine the format of the address
if address_info.Format == NET_ADDRESS_IPV4:
ip = IPv4Address(bytes(address_info.U.Ipv4Address))
elif address_info.Format == NET_ADDRESS_IPV6:
ip = IPv6Address(bytes(address_info.U.Ipv6Address))
elif address_info.Format == NET_ADDRESS_DNS_NAME:
ip = address_info.U.DnsName
else:
ip = "Unspecified"
return f"Format: {address_info.Format}, Address: {ip}, Port: {port_number.value}"
# Test the function with multiple network strings
network_strings = [
"192.168.100.10",
"192.168.100.10:80",
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2",
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A",
"[21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2]:8080",
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A:8080",
"www.microsoft.com",
"www.microsoft.com:80"
]
for ns in network_strings:
result = call_parse_network_string(ns)
print(f'"{ns}" --> {result}')
NetworkString | Format | Address | Port |
---|---|---|---|
"192.168.100.10" | NET_ADDRESS_IPV4 |
192.168.100.10 | 0 |
"192.168.100.10:80" | NET_ADDRESS_IPV4 |
192.168.100.10 | 80 |
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2" | NET_ADDRESS_IPV6 |
21da:d3:0:2f3b:2aa:ff:fe28:9c5a | 0 |
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A" | NET_ADDRESS_IPV6 |
21da:d3:0:2f3b:2aa:ff:fe28:9c5a | 0 |
"[21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A%2]:8080" | NET_ADDRESS_IPV6 |
21da:d3:0:2f3b:2aa:ff:fe28:9c5a | 8080 |
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A:8080" | The parameter is incorrect. | ||
"www.microsoft.com" | The parameter is incorrect. |
The documentation is simply incorrect. If you disassemble ParseNetworkString()
you will find it calls RtlIpv6StringToAddressEx()
which is documented as (emphasis added):
The string pointed to by the AddressString parameter must be represented in the form for an IPv6 address string followed by an optional percent character and scope ID string. The IPv6 address and scope ID string must be enclosed in square brackets. The right square bracket after the IPv6 address and scope ID string may be followed by an optional colon and a string representation of a port number.