winapiipv6winsock

ParseNetworkString will not parse 21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A:8080


I'm trying to find a way to parse a user entered IP address information, e.g.:

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:

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?

Bonus

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:

Complete Minimal Reproducible Example

# 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.

Solution

  • 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.