Skip to content

IPv6 Address Support in WinRM URLs #400

@illidan80

Description

@illidan80

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:

  1. IPv6 addresses:

    • http://[2a05:d018:1961:ba00:ff5b:37ba:20c7:726a]:5985/wsman
    • https://[fe80::1]:5986/wsman
  2. IPv4 addresses:

    • http://192.168.1.100:5985/wsman
    • https://10.0.0.1:5986/wsman
  3. Hostnames:

    • http://winserver.example.com:5985/wsman
    • https://windows-host:5986/wsman
  4. 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_url

Additional 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions