-
Notifications
You must be signed in to change notification settings - Fork 272
Description
IPv6 Address Support in WinRM URLs
Description
pywinrm fails to parse WinRM endpoint URLs that contain IPv6 addresses with bracket notation (RFC 3986 format). The Session._build_url() method uses a regex pattern that only matches IPv4 addresses and hostnames, causing IPv6 URLs to fail with parsing errors.
Environment
- pywinrm versions tested: 0.4.3, 0.5.0
- Python version: 3.9+
- Operating System: Amazon Linux 2, Windows Server 2019
- WinRM target: Windows Server with IPv6 address
Steps to Reproduce
import winrm
# Try to connect to Windows host using IPv6 address
session = winrm.Session(
target='http://[2a05:d018:1961:ba00:ff5b:37ba:20c7:726a]:5985/wsman',
auth=('Administrator', 'password'),
transport='ntlm',
server_cert_validation='ignore'
)
# Attempt to execute a command
result = session.run_cmd('echo test')Expected Behavior
The IPv6 URL should be parsed correctly:
- Scheme:
http - Host:
[2a05:d018:1961:ba00:ff5b:37ba:20c7:726a] - Port:
5985 - Path:
/wsman
And the WinRM connection should be established successfully.
Actual Behavior
The connection fails with an error like:
HTTPConnectionPool(host='http', port=5985): Max retries exceeded with url: /wsman
(Caused by NewConnectionError('<urllib3.connection.HTTPConnection object>:
Failed to establish a new connection: [Errno -2] Name or service not known'))
The URL is incorrectly parsed:
- Host:
http(wrong - treating protocol as hostname!) - Port:
5985
Root Cause
The issue is in winrm/session.py, in the _build_url() method:
@staticmethod
def _build_url(target, transport):
match = re.match(
r'(?i)^((?P<scheme>http[s]?)://)?(?P<host>[0-9a-z-_.]+)(:(?P<port>\d+))?(?P<path>(/)?(wsman)?)?',
target
)
# ...The regex pattern (?P<host>[0-9a-z-_.]+) only matches:
- Letters (a-z)
- Numbers (0-9)
- Hyphens, underscores, dots
It does NOT match:
- Square brackets
[ ](required for IPv6) - Colons
:(used in IPv6 addresses)
This causes IPv6 URLs like http://[2a05:...:726a]:5985/wsman to fail matching, and the regex groups become None, leading to incorrect URL construction.
Proposed Solution
Update the regex pattern in _build_url() to handle IPv6 addresses with bracket notation:
@staticmethod
def _build_url(target, transport):
# Try IPv6 pattern first
ipv6_match = re.match(
r'(?i)^((?P<scheme>http[s]?)://)?(\[(?P<ipv6>[0-9a-f:]+)\])(:(?P<port>\d+))?(?P<path>(/)?(wsman)?)?',
target
)
if ipv6_match:
scheme = ipv6_match.group('scheme') or ('https' if transport == 'ssl' else 'http')
host = '[' + ipv6_match.group('ipv6') + ']'
port = ipv6_match.group('port') or ('5986' if transport == 'ssl' else '5985')
path = ipv6_match.group('path') or 'wsman'
return '{0}://{1}:{2}/{3}'.format(scheme, host, port, path.lstrip('/'))
# Fall back to original pattern for IPv4/hostnames
match = re.match(
r'(?i)^((?P<scheme>http[s]?)://)?(?P<host>[0-9a-z-_.]+)(:(?P<port>\d+))?(?P<path>(/)?(wsman)?)?',
target
)
if not match:
raise ValueError(f"Invalid target URL: {target}")
scheme = match.group('scheme')
if not scheme:
scheme = 'https' if transport == 'ssl' else 'http'
host = match.group('host')
port = match.group('port')
if not port:
port = 5986 if transport == 'ssl' else 5985
path = match.group('path')
if not path:
path = 'wsman'
return '{0}://{1}:{2}/{3}'.format(scheme, host, port, path.lstrip('/'))Testing
This solution has been tested with:
-
IPv6 addresses:
http://[2a05:d018:1961:ba00:ff5b:37ba:20c7:726a]:5985/wsman✅https://[fe80::1]:5986/wsman✅
-
IPv4 addresses:
http://192.168.1.100:5985/wsman✅https://10.0.0.1:5986/wsman✅
-
Hostnames:
http://winserver.example.com:5985/wsman✅https://windows-host:5986/wsman✅
-
Short formats (without protocol/port/path):
[2a05:d018:1961:ba00::1]✅192.168.1.100✅hostname✅
Related Standards
- RFC 3986 - URI Generic Syntax: Defines IPv6 address format in URLs as
http://[IPv6address]:port/ - RFC 4007 - IPv6 Scoped Address Architecture
Impact
This bug prevents pywinrm from being used in IPv6-only or dual-stack environments where Windows hosts are only reachable via IPv6 addresses. As organizations transition to IPv6, this becomes increasingly important for automation and management tools.
Workaround
Users can temporarily monkey-patch the _build_url method:
import winrm
import re
_original_build_url = winrm.Session._build_url
@staticmethod
def _patched_build_url(target, transport):
# IPv6 pattern matching
ipv6_match = re.match(
r'(?i)^((?P<scheme>http[s]?)://)?(\[(?P<ipv6>[0-9a-f:]+)\])(:(?P<port>\d+))?(?P<path>(/)?(wsman)?)?',
target
)
if ipv6_match:
scheme = ipv6_match.group('scheme') or ('https' if transport == 'ssl' else 'http')
host = '[' + ipv6_match.group('ipv6') + ']'
port = ipv6_match.group('port') or ('5986' if transport == 'ssl' else '5985')
path = ipv6_match.group('path') or 'wsman'
return '{0}://{1}:{2}/{3}'.format(scheme, host, port, path.lstrip('/'))
return _original_build_url(target, transport)
winrm.Session._build_url = _patched_build_urlAdditional Context
Similar issues have been reported in other Python HTTP libraries and have been resolved by updating URL parsing to support IPv6 bracket notation. Examples:
- urllib3: Added IPv6 support in 1.19+
- requests: Supports IPv6 URLs natively via urllib3