HEX
Server: LiteSpeed
System: Linux s3512.bom1.stableserver.net 4.18.0-513.11.1.lve.el8.x86_64 #1 SMP Thu Jan 18 16:21:02 UTC 2024 x86_64
User: surajaut (1797)
PHP: 8.0.30
Disabled: NONE
Upload Files
File: //proc/thread-self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/modules/win_ip.py
"""
The networking module for Windows based systems
"""

import ipaddress
import logging
import textwrap

import salt.utils.network
import salt.utils.platform
import salt.utils.validate.net
import salt.utils.win_pwsh
from salt.exceptions import CommandExecutionError, SaltInvocationError

# Set up logging
log = logging.getLogger(__name__)

# Define the module's virtual name
__virtualname__ = "ip"


def __virtual__():
    """
    Confine this module to Windows systems
    """
    if not salt.utils.platform.is_windows():
        return False, "Module win_ip: Only available on Windows"
    if not salt.utils.win_pwsh.HAS_CLR:
        return False, "Module win_ip: Requires pythonnet (pip install pythonnet)"
    if not salt.utils.win_pwsh.HAS_PWSH_SDK:
        return (
            False,
            "Module win_ip: Requires the PowerShell SDK (System.Management.Automation)",
        )
    return __virtualname__


def _normalize_gateway_fields(data):
    """
    Ensure ``ipv4_gateways`` and ``ipv6_gateways`` are always lists.

    PowerShell 5.1's ``ConvertTo-Json`` unwraps single-element arrays to plain
    objects, so a single gateway arrives as a ``dict`` rather than a
    ``list[dict]``. This normalizes the parsed data in-place so callers always
    receive a consistent list type regardless of how many gateways are present.
    """
    for key in ("ipv4_gateways", "ipv6_gateways"):
        val = data.get(key)
        if isinstance(val, dict):
            data[key] = [val]
    return data


def _get_interfaces_legacy_format(name=None):
    """
    Returns interface data using the legacy netsh-style key names to avoid
    breaking existing scripts. The data is sourced from PowerShell objects,
    not netsh, so it is locale-independent.
    """
    if name:
        interfaces = get_interface_new(name)
    else:
        interfaces = list_interfaces(full=True)

    legacy = {}
    for name, data in interfaces.items():
        is_dhcp = data["ipv4_dhcp"]

        # Re-map to the specific netsh labels you had before
        legacy[name] = {
            "DHCP enabled": "Yes" if data["ipv4_dhcp"] else "No",
            "InterfaceMetric": data["ipv4_metric"],
            "Register with which suffix": (
                "Primary only" if data["dns_register"] else "None"
            ),
        }

        legacy[name]["ip_addrs"] = []
        if isinstance(data["ipv4_address"], str):
            data["ipv4_address"] = [data["ipv4_address"]]
        for addr in data["ipv4_address"]:
            ip_info = ipaddress.IPv4Interface(addr)
            legacy[name]["ip_addrs"].append(
                {
                    "IP Address": ip_info._string_from_ip_int(ip_info._ip),
                    "Netmask": str(ip_info.netmask),
                    "Subnet": str(ip_info.network),
                }
            )

        # Handle the dynamic Labeling for DNS/WINS
        if data["ipv4_dns"]:
            if isinstance(data["ipv4_dns"], str):
                dns_value = [data["ipv4_dns"]]
            else:
                dns_value = data["ipv4_dns"]
        else:
            dns_value = ["None"]
        if data["ipv4_wins"]:
            if isinstance(data["ipv4_wins"], str):
                wins_value = [data["ipv4_wins"]]
            else:
                wins_value = data["ipv4_wins"]
        else:
            wins_value = ["None"]
        if is_dhcp:
            legacy[name]["DNS servers configured through DHCP"] = dns_value
            legacy[name]["WINS servers configured through DHCP"] = wins_value
        else:
            legacy[name]["Statically Configured DNS Servers"] = dns_value
            legacy[name]["Statically Configured WINS Servers"] = wins_value

        # Add Gateway if it exists (ipv4_gateways is always a list after normalization)
        gws = [g for g in data["ipv4_gateways"] if g.get("ip")]
        if gws:
            legacy[name]["Default Gateway"] = gws[0]["ip"]
            legacy[name]["Gateway Metric"] = gws[0]["metric"]

    return legacy


def raw_interface_configs():
    """
    Return raw configs for all interfaces as returned by netsh. This command is
    localized and will return different text depending on the locality of the
    operating system.

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.raw_interface_configs
    """
    cmd = ["netsh", "interface", "ip", "show", "config"]
    return __salt__["cmd.run"](cmd, python_shell=False)


def get_all_interfaces():
    """
    Return IP configuration for all network interfaces using the legacy
    ``netsh``-compatible data format.

    Each interface is keyed by its alias and contains ``ip_addrs``,
    ``ipv4_gateway``, ``dns_servers``, and related fields mirroring the
    structure formerly produced by ``netsh interface ip show config``.
    Prefer :func:`list_interfaces` with ``full=True`` for richer, always-
    English output.

    Returns:
        dict: A dictionary keyed by interface name.  Each value is a
        dict with the legacy ``netsh``-style fields.

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.get_all_interfaces
    """
    return _get_interfaces_legacy_format()


def get_interface(iface):
    """
    Return the IP configuration of a single network interface

    Args:

        iface (str): The name of the interface

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.get_interface 'Local Area Connection'
    """
    return _get_interfaces_legacy_format(iface)


def is_enabled(iface):
    """
    Returns ``True`` if interface is enabled, otherwise ``False``

    Args:

        iface (str): The name of the interface to manage

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.is_enabled 'Local Area Connection #2'
    """
    with salt.utils.win_pwsh.PowerShellSession() as session:
        # Using SilentlyContinue ensures we get None/Empty if 'junk' is passed
        cmd = f"""
            [int](Get-NetAdapter -Name '{iface}' `
                            -ErrorAction SilentlyContinue).AdminStatus
        """
        status = session.run(cmd)

    try:
        # Use 0 as a fallback for None to trigger your "not found" check
        status = int(status if status is not None else 0)
    except (ValueError, TypeError):
        msg = f"Interface '{iface}' not found or invalid response."
        raise CommandExecutionError(msg)

    if status == 0:
        raise CommandExecutionError(f"Interface '{iface}' not found")

    return status == 1  # 1 is enabled


def is_disabled(iface):
    """
    Returns ``True`` if interface is disabled, otherwise ``False``

    Args:

        iface (str): The name of the interface to manage

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.is_disabled 'Local Area Connection #2'
    """
    with salt.utils.win_pwsh.PowerShellSession() as session:
        # Using SilentlyContinue ensures we get None/Empty if 'junk' is passed
        cmd = f"""
            [int](Get-NetAdapter -Name '{iface}' `
                                 -ErrorAction SilentlyContinue).AdminStatus
        """
        status = session.run(cmd)

    try:
        # Use 0 as a fallback for None to trigger your "not found" check
        status = int(status if status is not None else 0)
    except (ValueError, TypeError):
        msg = f"Interface '{iface}' not found or invalid response."
        raise CommandExecutionError(msg)

    if status == 0:
        raise CommandExecutionError(f"Interface '{iface}' not found")

    return status == 2  # 2 is disabled


def enable(iface):
    """
    Enable an interface

    Args:

        iface (str): The name of the interface to manage

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.enable 'Local Area Connection #2'
    """
    set_interface(iface, enabled=True)


def disable(iface):
    """
    Disable an interface

    Args:

        iface (str): The name of the interface to manage

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.disable 'Local Area Connection #2'
    """
    set_interface(iface, enabled=False)


def get_subnet_length(mask):
    """
    Convenience function to convert the netmask to the CIDR subnet length

    Args:

        mask (str): A netmask

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.get_subnet_length 255.255.255.0
    """
    if not salt.utils.validate.net.netmask(mask):
        raise SaltInvocationError(f"'{mask}' is not a valid netmask")
    return salt.utils.network.get_net_size(mask)


def set_static_ip(iface, addr, gateway=None, append=False):
    """
    Set static IP configuration on a Windows NIC

    Args:

        iface (str):
            The name of the interface to manage

        addr (str):
            IP address with subnet length (ex. ``10.1.2.3/24``). The
            :mod:`ip.get_subnet_length <salt.modules.win_ip.get_subnet_length>`
            function can be used to calculate the subnet length from a netmask.

        gateway (:obj:`str`, optional):
            If specified, the default gateway will be set to this value.
            Default is ``None``.

        append (:obj:`bool`, optional):
            If ``True``, the address will be added as a secondary IP to the
            interface. If ``False``, all existing IPv4 addresses are cleared
            first. Defaults to ``False``.

    Returns:
        dict: A dictionary with the applied settings, e.g.::

            {"Address Info": ["192.168.1.5/24"], "Default Gateway": "192.168.1.1"}

        ``Default Gateway`` is only present when the ``gateway`` argument is
        provided.

    Raises:
        SaltInvocationError: If ``addr`` or ``gateway`` is not a valid IPv4
            address.
        CommandExecutionError: If the address already exists on the interface.

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.set_static_ip 'Local Area Connection' 10.1.2.3/24 gateway=10.1.2.1
        salt -G 'os_family:Windows' ip.set_static_ip 'Local Area Connection' 10.1.2.4/24 append=True
    """
    if not salt.utils.validate.net.ipv4_addr(addr):
        raise SaltInvocationError(f"Invalid address '{addr}'")

    if gateway and not salt.utils.validate.net.ipv4_addr(gateway):
        raise SaltInvocationError(f"Invalid default gateway '{gateway}'")

    if "/" not in addr:
        addr += "/24"

    ip, _ = addr.split("/") if "/" in addr else (addr, "24")

    with salt.utils.win_pwsh.PowerShellSession() as session:

        # Get interface Index
        index = get_interface_index(iface, session)

        cmd = f"""
            (Get-NetIPAddress -InterfaceIndex {index} `
                              -AddressFamily IPv4 `
                              -ErrorAction SilentlyContinue).IPAddress
        """
        result = session.run(cmd)

        if result is None:
            exists = False
        elif isinstance(result, list):
            exists = ip in result
        else:
            exists = ip == result.strip()

    if exists:
        msg = f"Address '{ip}' already exists on '{iface}'"
        raise CommandExecutionError(msg)

    set_interface(
        iface=iface,
        ipv4_address=addr,
        ipv4_gateways=gateway if gateway else None,
        append=append,
    )
    # Verify new settings
    new_settings = get_interface_new(iface)[iface]

    ret = {"Address Info": new_settings["ipv4_address"]}
    if gateway:
        gws = [g for g in new_settings["ipv4_gateways"] if g.get("ip")]
        if gws:
            ret["Default Gateway"] = gws[0]["ip"]

    return ret


def set_dhcp_ip(iface):
    """
    Set Windows NIC to get IP from DHCP

    Args:

        iface (str):
            The name of the interface to manage

    Returns:
        dict: ``{}`` if DHCP was already enabled, otherwise
        ``{"Interface": <name>, "DHCP enabled": "Yes"}``.

    Raises:
        CommandExecutionError: If DHCP cannot be enabled.

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.set_dhcp_ip 'Local Area Connection'
    """
    with salt.utils.win_pwsh.PowerShellSession() as session:
        index = get_interface_index(iface, session)

        # Check if dhcp is already enabled
        cmd = f"""
            [int](Get-NetIPInterface -InterfaceIndex {index} `
                                -AddressFamily IPv4 `
                                -ErrorAction SilentlyContinue).Dhcp
        """
        dhcp_enabled = session.run(cmd)

        if dhcp_enabled == 1:
            return {}

    # Enable DHCP — set_interface manages its own session
    set_interface(iface, ipv4_dhcp=True)

    with salt.utils.win_pwsh.PowerShellSession() as session:
        index = get_interface_index(iface, session)

        # Verify that dhcp is enabled
        cmd = f"""
            (Get-NetIPInterface -InterfaceIndex {index} `
                                -AddressFamily IPv4 `
                                -ErrorAction SilentlyContinue).Dhcp
        """
        dhcp_enabled = session.run_json(cmd)
        if dhcp_enabled == 1:
            return {"Interface": iface, "DHCP enabled": "Yes"}
        else:
            raise CommandExecutionError("Failed to enable DHCP")


def set_static_dns(iface, *addrs):
    """
    Set static DNS configuration on a Windows NIC

    Args:

        iface (str): The name of the interface to manage

        addrs (list):
            One or more DNS servers to be added. To clear the list of DNS
            servers pass an empty list (``[]``). If undefined or ``None`` no
            changes will be made.

    Returns:
        dict: A dictionary containing the new DNS settings

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.set_static_dns 'Local Area Connection' '192.168.1.1'
        salt -G 'os_family:Windows' ip.set_static_dns 'Local Area Connection' '192.168.1.252' '192.168.1.253'
    """
    # Addrs is undefined or None, No Changes
    if not addrs or str(addrs[0]).lower() == "none":
        return {"Interface": iface, "DNS Server": "No Changes"}

    # Clear the list of DNS servers if [] is passed
    if str(addrs[0]).lower() == "[]":
        # Use set_dhcp_dns to reset the interface
        return set_dhcp_dns(iface)

    # Get interface Index
    index = get_interface_index(iface)

    with salt.utils.win_pwsh.PowerShellSession() as session:
        # 1. Fetch current DNS to see if work is actually needed
        cmd = f"""
            (Get-DnsClientServerAddress -InterfaceIndex {index} `
                                        -AddressFamily IPv4 `
                                        -ErrorAction SilentlyContinue).ServerAddresses
        """
        current_dns = session.run(cmd)

        # If current_dns is None (empty stack), make it an empty list
        if current_dns is None:
            current_dns = []
        # If it's a string (single IP), put it in a list
        elif isinstance(current_dns, str):
            # Split by newlines/commas and strip whitespace
            current_dns = [
                d.strip()
                for d in current_dns.replace(",", "\n").splitlines()
                if d.strip()
            ]

        # 2. Check if there's anything to set
        requested_dns = list(addrs)
        if set(current_dns) == set(requested_dns):
            return {}

        # 3. Set static list (comma-separated for PowerShell)
        dns_str = ",".join([f"'{a}'" for a in requested_dns])
        cmd = f"""
            Set-DnsClientServerAddress -InterfaceIndex {index} `
                                       -ServerAddresses ({dns_str})
        """
        session.run(cmd)

        # 4. Verify successful changes
        cmd = f"""
            (Get-DnsClientServerAddress -InterfaceIndex {index} `
                                        -AddressFamily IPv4 `
                                        -ErrorAction SilentlyContinue).ServerAddresses
        """
        current_dns = session.run(cmd)
        # If current_dns is None (empty stack), make it an empty list
        if current_dns is None:
            current_dns = []
        # If it's a string (single IP), put it in a list
        elif isinstance(current_dns, str):
            # Split by newlines/commas and strip whitespace
            current_dns = [
                d.strip()
                for d in current_dns.replace(",", "\n").splitlines()
                if d.strip()
            ]
        if set(current_dns) == set(requested_dns):
            return {"Interface": iface, "DNS Server": current_dns}

    raise CommandExecutionError("Failed to set DNS.")


def set_dhcp_dns(iface):
    """
    Set DNS source to DHCP on Windows

    Args:

        iface (str): The name of the interface to manage

    Returns:
        dict: ``{}`` if DNS was already set to automatic (no static servers
        configured), otherwise ``{"Interface": <name>, "DNS Server": "DHCP (Empty)"}``.

    Raises:
        CommandExecutionError: If the DNS reset cannot be verified.

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.set_dhcp_dns 'Local Area Connection'
    """
    with salt.utils.win_pwsh.PowerShellSession() as session:
        # 1. Get the interface index
        index = get_interface_index(iface, session)

        # 2. Fetch current DNS to see if work is actually needed
        cmd = f"""
            (Get-DnsClientServerAddress -InterfaceIndex {index} `
                                        -AddressFamily IPv4 `
                                        -ErrorAction SilentlyContinue).ServerAddresses
        """
        current_dns = session.run(cmd)

        # If current_dns is None (empty stack), it's already set
        if not current_dns:
            return {}

        # 3. Apply the reset.
        # This is the PowerShell equivalent of clicking 'Obtain automatically'
        cmd = f"""
            Set-DnsClientServerAddress -InterfaceIndex {index} `
                                       -ResetServerAddresses
        """
        session.run(cmd)

        # 4. Verify by checking if ServerAddresses is now empty (or managed by DHCP)
        #    On many interfaces (like Loopback), a reset results in an empty list.
        cmd = f"""
            (Get-DnsClientServerAddress -InterfaceIndex {index} `
                                        -AddressFamily IPv4).ServerAddresses
        """
        res = session.run(cmd)

        # If it returns None or an empty string, it's successfully reset/empty
        if not res:
            return {"Interface": iface, "DNS Server": "DHCP (Empty)"}

    raise CommandExecutionError("Failed to set DNS source to DHCP.")


def set_dhcp_all(iface):
    """
    Set both IP Address and DNS to DHCP

    Args:

        iface (str): The name of the interface to manage

    Returns:
        dict: ``{"Interface": <name>, "DNS Server": "DHCP", "DHCP enabled": "Yes"}``

    Raises:
        CommandExecutionError: If either the IP or DNS cannot be switched to DHCP.

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.set_dhcp_all 'Local Area Connection'
    """
    set_dhcp_ip(iface)
    set_dhcp_dns(iface)
    return {"Interface": iface, "DNS Server": "DHCP", "DHCP enabled": "Yes"}


def get_default_gateway(iface=None):
    """
    Get the Default Gateway on Windows.

    Args:

        iface (str, optional):
            The name or alias of the interface to query (e.g., 'Ethernet').
            If provided, the function returns the default gateway specific
            to that interface. If ``None`` (default), it returns the
            system-wide primary default gateway based on the lowest route
            metric.

    Returns:
        str: The IP address of the default gateway.

    Raises:
        CommandExecutionError: If no default gateway is found.

    CLI Example:

    .. code-block:: bash

        # Get the system's primary default gateway
        salt 'minion' ip.get_default_gateway

        # Get the gateway for a specific interface
        salt 'minion' ip.get_default_gateway iface='Local Area Connection'
    """
    with salt.utils.win_pwsh.PowerShellSession() as session:

        if iface:
            index = get_interface_index(iface)
            cmd = f"""
                Get-NetRoute -DestinationPrefix '0.0.0.0/0' `
                             -InterfaceIndex {index} `
                             -ErrorAction SilentlyContinue |
                    Sort-Object RouteMetric |
                    Select-Object -First 1 -ExpandProperty NextHop
            """
        else:
            cmd = """
                Get-NetRoute -DestinationPrefix '0.0.0.0/0' `
                             -ErrorAction SilentlyContinue |
                    Sort-Object RouteMetric |
                    Select-Object -First 1 -ExpandProperty NextHop
            """

        gateway = session.run(cmd)

    if not gateway:
        raise CommandExecutionError("Unable to find default gateway")

    return gateway


def get_interface_index(iface, session=None):
    """
    Return the integer ``ifIndex`` for the named interface.

    Args:

        iface (str):
            The interface name or alias (e.g., ``'Ethernet'``).

        session (:class:`~salt.utils.win_pwsh.PowerShellSession`, optional):
            An existing ``PowerShellSession`` to reuse. If ``None`` (default),
            a new session is opened for this call only.

    Returns:
        int: The interface index.

    Raises:
        CommandExecutionError: If the interface cannot be found.

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.get_interface_index 'Ethernet'
    """
    cmd = f"""
        $adapter = Get-NetAdapter -Name "{iface}" `
                                  -ErrorAction SilentlyContinue
        if (-not $adapter) {{
            $adapter = Get-NetIPInterface -InterfaceAlias "{iface}" `
                                          -ErrorAction SilentlyContinue |
                Select-Object -First 1
        }}
        if ($adapter) {{ $adapter.ifIndex }} else {{ "0" }}
    """

    def _run(s):
        raw_index = s.run(cmd)
        try:
            index = int(raw_index)
        except (ValueError, TypeError):
            raise CommandExecutionError(
                f"Interface not found or not initialized: {iface}"
            )
        if index == 0:
            raise CommandExecutionError(f"Interface not found: {iface}")
        return index

    if session is not None:
        return _run(session)
    with salt.utils.win_pwsh.PowerShellSession() as s:
        return _run(s)


def set_interface(
    iface,
    alias=None,
    enabled=None,
    ipv4_enabled=None,
    ipv4_address=None,
    ipv4_dhcp=None,
    ipv4_dns=None,
    ipv4_forwarding=None,
    ipv4_gateways=None,
    ipv4_metric=None,
    ipv4_wins=None,
    ipv4_netbios=None,
    ipv6_enabled=None,
    ipv6_address=None,
    ipv6_dhcp=None,
    ipv6_dns=None,
    ipv6_forwarding=None,
    ipv6_gateways=None,
    ipv6_metric=None,
    dns_register=None,
    dns_suffix=None,
    mtu=None,
    append=False,
):
    """
    Configures a network interface on Windows.

    This function provides a context-aware interface for managing adapter
    properties, protocol bindings, and IP configurations. It utilizes the
    InterfaceIndex to ensure stability during renames.

    **Understanding Metrics on Windows:**
    Windows calculates route priority by summing the **Interface Metric**
    (protocol level) and the **Route Metric** (gateway level).

    * **Interface Metric:** Set via ``ipvX_metric``. A value of ``0``
      enables 'Automatic Metric', where Windows assigns priority based on link
      speed.
    * **Route Metric:** Set within the ``ipvX_gateways`` objects.
    * **Total Cost:** Interface Metric + Route Metric. The lowest total
      cost determines the primary route.

    **Context-Aware Behavior:**
    The function identifies the current state of protocol bindings (IPv4/IPv6)
    before applying settings.

    * If a protocol is disabled and ``ipvX_enabled`` is not passed as ``True``,
      configuration for that stack (IPs, DNS, etc.) is skipped to prevent
      errors.
    * If ``ipvX_dhcp`` is enabled, static IP and gateway configurations are
      ignored by the OS; therefore, this function skips applying static
      addresses unless DHCP is ``False``.

    Args:

        iface (str):
            The current name or alias of the interface (e.g., 'Ethernet').

        alias (str, optional):
            A new name for the interface.

        enabled (bool, optional):
            Administrative status of the adapter.

        ipv4_enabled (bool, optional):
            Whether the IPv4 protocol is bound to this adapter. If ``None``, the
            function discovers current state.

        ipv4_address (list, optional):
            An IPv4 address or list of addresses in CIDR notation
            (e.g., ``['192.168.1.5/24']``).

            .. note::
                If a CIDR prefix is not provided, it will default to ``/24``.

        ipv4_dhcp (bool, optional):
            Set to ``True`` to enable DHCP, ``False`` for static.

        ipv4_dns (list, optional):
            A list of IPv4 DNS server addresses. Passing an empty list ``[]``
            resets DNS to automatic.

        ipv4_forwarding (bool, optional):
            Enables or disables **IP Forwarding** for the IPv4 stack. When
            ``True``, this allows the Windows machine to act as a router,
            passing traffic between this interface and others.
            Default is ``None`` (no change).

        ipv4_gateways (list, str, dict):
            The default gateway(s). Accepts multiple formats:

            * **String:** A single IP address (e.g., ``'192.168.1.1'``).
            * **List of Strings:** Multiple gateways (e.g., ``['1.1.1.1', '1.0.0.1']``).
            * **Dictionary:** A single gateway with a specific route metric
              (e.g., ``{'ip': '192.168.1.1', 'metric': 5}``).
            * **List of Dictionaries:** Multiple gateways with individual metrics
              (e.g., ``[{'ip': '1.1.1.1', 'metric': 2}, {'ip': '1.0.0.1', 'metric': 10}]``).

            If a metric is not specified within a dictionary, or if a string/list
            of strings is provided, the function falls back to ``ipv4_metric``.

        ipv4_metric (int, optional):
            The IPv4 interface metric. Use ``0`` for Automatic.

        ipv4_wins (list, optional):
            A list of up to two WINS server addresses.

        ipv4_netbios (str, optional):
            Configures NetBIOS over TCP/IP behavior via the MSFT_NetIPInterface
            CIM class.

            * ``Default`` (0): Defer to DHCP server settings.
            * ``Enable`` (1): Explicitly enable NetBIOS.
            * ``Disable`` (2): Explicitly disable NetBIOS.

        ipv6_enabled (bool, optional):
            Whether the IPv6 protocol is bound.

        ipv6_address (list, optional):
            An IPv6 address or list of addresses in CIDR notation
            (e.g., ``['2001:db8::1/64']``).

            .. note::
                If a CIDR prefix is not provided, it will default to ``/64``.

        ipv6_dhcp (bool, optional):
            Set to ``True`` for IPv6 stateful configuration.

        ipv6_dns (list, optional):
            A list of IPv6 DNS server addresses.

        ipv6_forwarding (bool, optional):
            Enables or disables **IP Forwarding** for the IPv6 stack. When
            ``True``, this allows the Windows machine to act as a router,
            passing traffic between this interface and others.
            Default is ``None`` (no change).

        ipv6_gateways (list, optional):
            The default gateway(s) for IPv6. Accepts multiple formats:

            * **String:** A single IPv6 address (e.g., ``'2001:db8::1'``).
            * **List of Strings:** Multiple gateways (e.g., ``['2001:db8::1', 'fe80::1']``).
            * **Dictionary:** A single gateway with a specific route metric
              (e.g., ``{'ip': '2001:db8::1', 'metric': 5}``).
            * **List of Dictionaries:** Multiple gateways with individual metrics
              (e.g., ``[{'ip': '2001:db8::1', 'metric': 2}, {'ip': 'fe80::1', 'metric': 10}]``).

            If a metric is not specified within a dictionary, or if a string/list
            of strings is provided, the function falls back to ``ipv6_metric``.
            Note that link-local gateways (starting with ``fe80::``) are fully
            supported as Windows scopes them via the interface index.

        ipv6_metric (int, optional):
            The IPv6 interface metric. Use ``0`` for Automatic.

        dns_register (bool, optional):
            Controls whether the interface's IP addresses are registered in DNS
            using the computer's primary DNS suffix.

            * ``True`` (Default in Windows): Corresponds to "Primary only" in
              legacy output. The computer will attempt to dynamically update
              its DNS record (A/AAAA) on the DNS server.
            * ``False``: Corresponds to "None". The computer will not attempt
              to register its name for this specific connection.

        dns_suffix (str, optional):
            Sets the **Connection-Specific DNS Suffix** (e.g.,
            ``corp.example.com``). This value is used in DNS registration and
            resolution but does not change the global primary DNS suffix of the
            computer.

        mtu (int, optional):
            Configures the **Maximum Transmission Unit** size for the physical
            adapter. Accepts values between ``576`` and ``9000``. Common uses
            include setting ``9000`` for Jumbo Frames in iSCSI/Storage networks
            or lower values for VPN compatibility.

        append (bool):
            If ``True``, the provided IPv4 and IPv6 addresses will be added
            to the interface without removing existing ones. If ``False`` (default),
            all existing IPv4/IPv6 addresses on the interface will be removed
            before applying the new configuration.

            .. note::
                This flag only applies to IP addresses. Gateways (Default Routes)
                are always replaced if a new gateway is provided to ensure
                routing stability.

    Returns:
        bool: ``True`` if successful, otherwise raises an exception.

    CLI Examples:

    .. code-block:: bash

        # Set static IPv4 with a specific metric
        salt-call --local win_ip.set_interface "Ethernet" ipv4_dhcp=False \\
            ipv4_address="['192.168.1.10/24']" ipv4_metric=10

        # Set multiple gateways with different route priorities
        salt-call --local win_ip.set_interface "Test-Iface" \\
            ipv4_gateways="[{'ip': '10.0.0.1', 'metric': 2}, {'ip': '10.0.0.2', 'metric': 100}]"

        # Reset DNS to automatic/DHCP
        salt '*' win_ip.set_interface "Wi-Fi" ipv4_dns="[]"

        # Rename an interface and enable IPv6
        salt-call --local win_ip.set_interface "Ethernet 2" alias="Production" \\
            ipv6_enabled=True ipv6_dhcp=True
    """
    # 1. Input Validation
    if ipv4_netbios and ipv4_netbios.lower() not in ["default", "enable", "disable"]:
        raise SaltInvocationError(f"Invalid NetBIOS setting: {ipv4_netbios}")

    if mtu is not None and (mtu < 576 or mtu > 9000):
        raise SaltInvocationError("MTU must be between 576 and 9000.")

    ipv4_addrs = (
        ipv4_address
        if isinstance(ipv4_address, list)
        else ([ipv4_address] if ipv4_address else [])
    )
    ipv6_addrs = (
        ipv6_address
        if isinstance(ipv6_address, list)
        else ([ipv6_address] if ipv6_address else [])
    )

    # 1. Identity & Setup
    index = get_interface_index(iface)

    ps_script = textwrap.dedent(
        f"""
        $idx = {index}
        $ErrorActionPreference = 'Stop'
        # Internal Sanitizer for DHCP transitions
        function Sanitize-Stack {{
            param([int]$i, [string]$family)
            $stack = if($family -eq 'IPv4') {{ 'Tcpip' }} else {{ 'Tcpip6' }}
            $guid = (Get-NetAdapter -InterfaceIndex $i).DeviceGuid
            $reg = "HKLM:\\\\SYSTEM\\\\CurrentControlSet\\\\Services\\\\$stack\\\\Parameters\\\\Interfaces\\\\$guid"
            if (Test-Path $reg) {{
                Set-ItemProperty $reg 'NameServer' '' -ErrorAction SilentlyContinue
                Set-ItemProperty $reg 'DefaultGateway' ([string[]]@()) -ErrorAction SilentlyContinue
                if ($family -eq 'IPv4') {{
                    Set-ItemProperty $reg 'WINSPrimaryServer' '' -ErrorAction SilentlyContinue
                    Set-ItemProperty $reg 'WINSSecondaryServer' '' -ErrorAction SilentlyContinue
                    $bt = "HKLM:\\\\SYSTEM\\\\CurrentControlSet\\\\Services\\\\NetBT\\\\Parameters\\\\Interfaces\\\\TcpIp_$guid"
                    if (Test-Path $bt) {{ Set-ItemProperty $bt 'NameServerList' ([string[]]@()) -ErrorAction SilentlyContinue }}
                    # THE NUCLEAR ADDITION: Flush the WMI Cache specifically
                    $wmi = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "InterfaceIndex = $i" -ErrorAction SilentlyContinue
                    if ($wmi) {{
                        $wmi | Invoke-CimMethod -MethodName SetWINSServer -Arguments @{{WINSPrimaryServer=''; WINSSecondaryServer=''}} -ErrorAction SilentlyContinue
                    }}
                }}
            }}
            $prefix = if($family -eq 'IPv4') {{ '0.0.0.0/0' }} else {{ '::/0' }}
            $r = try {{ Get-NetRoute -InterfaceIndex $idx -DestinationPrefix $prefix -ErrorAction SilentlyContinue }} catch {{ $null }}
            if ($r) {{ try {{ $r | Remove-NetRoute -Confirm:$false }} catch {{ }} }}
            $a = try {{ Get-NetIPAddress -InterfaceIndex $idx -AddressFamily $family -ErrorAction SilentlyContinue | Where-Object {{ $_.PrefixOrigin -eq 'Manual' }} }} catch {{ $null }}
            if ($a) {{ try {{ $a | Remove-NetIPAddress -Confirm:$false }} catch {{ }} }}
            Set-DnsClientServerAddress -InterfaceIndex $idx -ResetServerAddresses -ErrorAction SilentlyContinue
        }}
    """
    )

    # 2. Administrative State & MTU
    if enabled is not None:
        action = "Enable-NetAdapter" if enabled else "Disable-NetAdapter"
        ps_script += textwrap.dedent(
            f"""
            Get-NetAdapter -InterfaceIndex $idx | {action} -Confirm:$false"""
        )

    if mtu is not None:
        ps_script += textwrap.dedent(
            f"""
            Set-NetIPInterface -InterfaceIndex $idx `
                               -AddressFamily IPv4 `
                               -NlMtuBytes {mtu} `
                               -Confirm:$false"""
        )
        # Applying to IPv6 as well for a consistent MTU across the interface
        ps_script += textwrap.dedent(
            f"""
            Set-NetIPInterface -InterfaceIndex $idx `
                               -AddressFamily IPv6 `
                               -NlMtuBytes {mtu} `
                               -Confirm:$false `
                               -ErrorAction SilentlyContinue"""
        )

    # 3. Protocol Binding & Basic Interface Settings
    for family, active, dhcp, forward, metric in [
        ("IPv4", ipv4_enabled, ipv4_dhcp, ipv4_forwarding, ipv4_metric),
        ("IPv6", ipv6_enabled, ipv6_dhcp, ipv6_forwarding, ipv6_metric),
    ]:
        if active is not None:
            comp = "ms_tcpip" if family == "IPv4" else "ms_tcpip6"
            action = (
                "Enable-NetAdapterBinding" if active else "Disable-NetAdapterBinding"
            )
            # SilentlyContinue: the binding toggle is idempotent and some adapter
            # types (e.g. loopback) emit non-fatal errors for already-set states.
            ps_script += textwrap.dedent(
                f"""
                Get-NetAdapter -InterfaceIndex $idx | {action} -ComponentID '{comp}' -ErrorAction SilentlyContinue"""
            )

        # IP Interface Properties (DHCP, Forwarding, Metric)
        if any(x is not None for x in [dhcp, forward, metric]):
            cmd = f"Set-NetIPInterface -InterfaceIndex $idx -AddressFamily {family}"
            if dhcp is not None:
                cmd += f" -Dhcp {'Enabled' if dhcp else 'Disabled'}"
            if forward is not None:
                cmd += f" -Forwarding {'Enabled' if forward else 'Disabled'}"
            if metric is not None:
                if metric == 0:
                    cmd += " -AutomaticMetric Enabled"
                else:
                    cmd += f" -AutomaticMetric Disabled -InterfaceMetric {metric}"
            ps_script += textwrap.dedent(
                f"""
                {cmd}"""
            )

            # Trigger Sanitizer if enabling DHCP
            if dhcp:
                ps_script += textwrap.dedent(
                    f"""
                    Sanitize-Stack -i $idx -family '{family}'"""
                )

    # 4. IP Addresses (Static)
    for family, addrs, dhcp in [
        ("IPv4", ipv4_addrs, ipv4_dhcp),
        ("IPv6", ipv6_addrs, ipv6_dhcp),
    ]:
        if addrs and not dhcp:
            if not append:
                ps_script += textwrap.dedent(
                    f"""
                    $currentIPs = try {{
                        Get-NetIPAddress -InterfaceIndex $idx `
                                         -AddressFamily {family} `
                                         -ErrorAction SilentlyContinue |
                        Where-Object {{ $_.IPAddress -notlike 'fe80*' -and $_.PrefixOrigin -eq 'Manual' }}
                    }} catch {{ $null }}
                    if ($currentIPs) {{
                        try {{ $currentIPs | Remove-NetIPAddress -Confirm:$false }} catch {{ }}
                    }}
                """
                )

            # 'addrs' is now GUARANTEED to be a list of strings
            for a in addrs:
                # Split logic
                parts = a.split("/")
                ip = parts[0]
                pref = (
                    parts[1] if len(parts) > 1 else ("24" if family == "IPv4" else "64")
                )

                ps_script += textwrap.dedent(
                    f"""
                    New-NetIPAddress -InterfaceIndex $idx `
                                     -IPAddress '{ip}' `
                                     -PrefixLength {pref} `
                                     -AddressFamily {family}
                """
                )

    # 5. Gateways (Default Routes)
    for family, gateways, metric_fallback in [
        ("IPv4", ipv4_gateways, ipv4_metric),
        ("IPv6", ipv6_gateways, ipv6_metric),
    ]:
        if gateways is not None:
            prefix = "0.0.0.0/0" if family == "IPv4" else "::/0"
            # Clear existing default routes first
            ps_script += textwrap.dedent(
                f"""
                $routes = try {{
                    Get-NetRoute -InterfaceIndex $idx `
                                 -DestinationPrefix '{prefix}' `
                                 -ErrorAction SilentlyContinue
                }} catch {{ $null }}
                if ($routes) {{
                    try {{ $routes | Remove-NetRoute -Confirm:$false }} catch {{ }}
                }}"""
            )

            gw_list = gateways if isinstance(gateways, list) else [gateways]
            for gw in gw_list:
                if isinstance(gw, dict):
                    ip, met = gw.get("ip"), gw.get(
                        "metric", metric_fallback if metric_fallback else 0
                    )
                else:
                    ip, met = gw, (metric_fallback if metric_fallback else 0)
                if ip:
                    ps_script += textwrap.dedent(
                        f"""
                        New-NetRoute -InterfaceIndex $idx `
                                     -DestinationPrefix '{prefix}' `
                                     -NextHop '{ip}' `
                                     -RouteMetric {met} `
                                     -Confirm:$false"""
                    )

    # 6. DNS, WINS, and NetBIOS
    if dns_suffix:
        ps_script += textwrap.dedent(
            f"""
            Set-DnsClient -InterfaceIndex $idx `
                          -ConnectionSpecificSuffix '{dns_suffix}'"""
        )
    if dns_register is not None:
        ps_script += textwrap.dedent(
            f"""
            Set-DnsClient -InterfaceIndex $idx `
                          -RegisterThisConnectionsAddress {'$true' if dns_register else '$false'}"""
        )

    for family, dns_servers in [("IPv4", ipv4_dns), ("IPv6", ipv6_dns)]:
        if dns_servers is not None:
            if not dns_servers:
                ps_script += textwrap.dedent(
                    f"""
                    Set-DnsClientServerAddress -InterfaceIndex $idx `
                                               -AddressFamily {family} `
                                               -ResetServerAddresses"""
                )
            else:
                s_list = dns_servers if isinstance(dns_servers, list) else [dns_servers]
                # 1. Format the PowerShell array string first
                # Result: "@('8.8.8.8','1.1.1.1')"
                dns_array_str = "@(" + ",".join([f"'{s}'" for s in s_list]) + ")"
                ps_script += textwrap.dedent(
                    f"""
                    Set-DnsClientServerAddress -InterfaceIndex $idx `
                                               -ServerAddresses {dns_array_str}"""
                )

    if ipv4_wins is not None:
        w = ipv4_wins if isinstance(ipv4_wins, list) else [ipv4_wins]
        p, s = (w[0] if len(w) > 0 else ""), (w[1] if len(w) > 1 else "")
        ps_script += textwrap.dedent(
            f"""
            $w = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "InterfaceIndex = $idx"
            if($w) {{
                $w | Invoke-CimMethod -MethodName SetWINSServer `
                                      -Arguments @{{WINSPrimaryServer='{p}'; WINSSecondaryServer='{s}'}}
            }}"""
        )

    if ipv4_netbios:
        nb_val = {"default": 0, "enable": 1, "disable": 2}[ipv4_netbios.lower()]
        ps_script += textwrap.dedent(
            f"""
            Set-NetIPInterface -InterfaceIndex $idx `
                               -AddressFamily IPv4 `
                               -NetbiosSetting {nb_val}"""
        )

    # 7. Rename & Finalize
    if alias and alias != iface:
        ps_script += textwrap.dedent(
            f"""
            Get-NetAdapter -InterfaceIndex $idx |
            Rename-NetAdapter -NewName '{alias}' `
                              -Confirm:$false"""
        )

    ps_script += textwrap.dedent(
        """
    $iface = Get-NetAdapter -InterfaceIndex $idx `
                            -ErrorAction SilentlyContinue
    if ($iface.AdminStatus -eq 1 -and $iface.MediaConnectionState -eq 1) {
        $timeout = [System.Diagnostics.Stopwatch]::StartNew()
        while ($timeout.Elapsed.TotalSeconds -lt 10) {
            # Check for any addresses still in the 'Tentative' state.
            # Wrap in try/catch: Get-NetIPAddress throws a terminating CIM error
            # when the IP stack is disabled, which -ErrorAction cannot suppress.
            $tentative = try {
                Get-NetIPAddress -InterfaceIndex $idx -ErrorAction SilentlyContinue |
                Where-Object { $_.AddressState -eq 'Tentative' }
            } catch { $null }
            if (-not $tentative) {
                break
            }
            Start-Sleep -Milliseconds 200
        }
    }
    """
    )

    # 8. Execution
    with salt.utils.win_pwsh.PowerShellSession() as session:
        session.import_modules(["NetAdapter", "NetTCPIP", "DnsClient"])
        session.run_strict(ps_script)

    return True


def get_interface_new(iface):
    """
    Retrieves the current configuration of a network interface on Windows.

    This function gathers comprehensive data about a network adapter, including
    administrative status, protocol bindings, IP addresses, DNS servers,
    gateways, and WINS configuration. The returned dictionary is structured
    to be directly compatible with the parameters of ``set_interface``.

    **Data Structures and Round-trip Logic:**

    * **Addresses:** IPs are returned in CIDR notation (e.g., ``10.0.0.5/24``)
      to ensure the setter can accurately recreate the subnet mask.
    * **Gateways:** Returned as a list of dictionaries containing both the
      IP (``ip``) and the route-specific metric (``metric``).
    * **Metrics:** A value of ``0`` indicates that the interface is
      configured for 'Automatic Metric' calculation by Windows.

    Args:

        iface (str):
            The name or alias of the interface (e.g., 'Ethernet'). This is used
            for the initial hardware lookup to find the permanent ``InterfaceIndex``.

    Returns:
        dict: A dictionary keyed by the interface name containing:

            - **alias** (str): The friendly name of the adapter.
            - **description** (str): Human-readable adapter description
              (e.g., ``"Microsoft Loopback Adapter"``).
            - **enabled** (bool): Administrative status (``True`` = Up).
            - **index** (int): The stable ``InterfaceIndex`` used internally
              by all CIM/PowerShell cmdlets.
            - **link_status** (str): Current link state reported by the driver
              (e.g., ``"Up"``, ``"Disconnected"``).
            - **mac_address** (str): The physical (MAC) address of the adapter.
            - **mtu** (int): MTU in bytes; defaults to ``1500`` when the
              adapter reports no value (``4294967295`` or ``None``).
            - **dns_register** (bool): ``True`` if the adapter registers its
              addresses in DNS using the computer's primary DNS suffix.
            - **dns_suffix** (str): Connection-specific DNS suffix, or
              ``None`` if not set.
            - **ipv4_enabled** (bool): Status of the IPv4 protocol binding.
            - **ipv4_dhcp** (bool): ``True`` if IPv4 DHCP is enabled.
            - **ipv4_metric** (int): The IPv4 interface metric (``0`` = Auto).
            - **ipv4_address** (list): List of IPv4 addresses in CIDR format.
            - **ipv4_gateways** (list): List of dicts, each with ``ip`` and
              ``metric`` keys, for IPv4 default routes. Always a list — even
              when only one gateway is configured — because
              :func:`_normalize_gateway_fields` corrects the PowerShell 5.1
              behavior of unwrapping single-element arrays.
            - **ipv4_dns** (list): List of IPv4 DNS server addresses.
            - **ipv4_forwarding** (bool | None): ``True`` if IPv4 forwarding
              is enabled, ``None`` if the IPv4 stack is not bound.
            - **ipv4_wins** (list): Primary and secondary WINS server
              addresses (empty list if none configured).
            - **ipv4_netbios** (str): NetBIOS configuration —
              ``"Default"``, ``"Enable"``, or ``"Disable"``.
            - **ipv6_enabled** (bool): Status of the IPv6 protocol binding.
            - **ipv6_dhcp** (bool): ``True`` if IPv6 stateful config is
              enabled.
            - **ipv6_metric** (int): The IPv6 interface metric (``0`` = Auto).
            - **ipv6_address** (list): List of IPv6 addresses in CIDR format.
            - **ipv6_gateways** (list): List of dicts, each with ``ip`` and
              ``metric`` keys, for IPv6 default routes. Always a list for the
              same reason as ``ipv4_gateways``.
            - **ipv6_dns** (list): List of IPv6 DNS server addresses.
            - **ipv6_forwarding** (bool | None): ``True`` if IPv6 forwarding
              is enabled, ``None`` if the IPv6 stack is not bound.

    Raises:
        CommandExecutionError: If the specified interface cannot be found.

    CLI Examples:

    .. code-block:: bash

        # Get details for a specific interface
        salt-call --local win_ip.get_interface_new "Ethernet"

        # Get details for the loopback test adapter
        salt '*' win_ip.get_interface_new "Test-Iface"
    """
    index = get_interface_index(iface)

    # ONE script to rule them all
    cmd = rf"""
        $idx = {index}
        $adapter = Get-NetAdapter -InterfaceIndex $idx -ErrorAction SilentlyContinue
        if (-not $adapter) {{ return @{{}} | ConvertTo-Json }}

        $ipv4 = Get-NetIPInterface -InterfaceIndex $idx -AddressFamily IPv4 -ErrorAction SilentlyContinue
        $ipv6 = Get-NetIPInterface -InterfaceIndex $idx -AddressFamily IPv6 -ErrorAction SilentlyContinue
        $dns  = Get-DnsClientServerAddress -InterfaceIndex $idx -ErrorAction SilentlyContinue
        $reg  = Get-DnsClient -InterfaceIndex $idx -ErrorAction SilentlyContinue
        $wmi  = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "InterfaceIndex = $idx" -ErrorAction SilentlyContinue

        $rawMtu = if ($ipv4) {{ $ipv4.NlMtu }} elseif ($ipv6) {{ $ipv6.NlMtu }} else {{ $adapter.MtuSize }}
        $finalMtu = if ($rawMtu -eq 4294967295 -or $null -eq $rawMtu) {{ 1500 }} else {{ $rawMtu }}

        # Helper to format IPs into CIDR
        function Get-CIDR {{
            param($family)
            $ips = Get-NetIPAddress -InterfaceIndex $idx -AddressFamily $family -ErrorAction SilentlyContinue |
                   Where-Object {{ $_.AddressState -eq 'Preferred' -and $_.IPAddress -notlike 'fe80*' }}
            if ($ips) {{ @($ips | ForEach-Object {{ "$($_.IPAddress)/$($_.PrefixLength)" }}) }} else {{ @() }}
        }}

        # Helper to format Gateways
        function Get-Gateways {{
            param($family)
            $prefix = if($family -eq 'IPv4') {{ "0.0.0.0/0" }} else {{ "::/0" }}
            $routes = Get-NetRoute -InterfaceIndex $idx -DestinationPrefix $prefix -ErrorAction SilentlyContinue |
                      Where-Object {{ $_.NextHop -and $_.NextHop -notmatch '^0\.0\.0\.0$|^::$' }}
            if ($routes) {{
                @($routes | Select-Object @{{Name='ip';Expression={{$_.NextHop}}}}, @{{Name='metric';Expression={{$_.RouteMetric}}}})
            }} else {{ @() }}
        }}

        $result = [PSCustomObject]@{{
            alias           = $adapter.Name
            description     = $adapter.InterfaceDescription
            dns_register    = $reg.RegisterThisConnectionsAddress
            dns_suffix      = $reg.ConnectionSpecificSuffix
            enabled         = $adapter.AdminStatus -eq 1
            index           = $idx
            link_status     = $adapter.Status
            mac_address     = $adapter.MacAddress
            mtu             = [int]$finalMtu

            # IPv4 Stack
            ipv4_address    = Get-CIDR -family IPv4
            ipv4_dhcp       = if($ipv4) {{ $ipv4.Dhcp -eq 1 }} else {{ $false }}
            ipv4_dns        = @($dns | Where-Object {{$_.AddressFamily -eq 2}} | Select-Object -ExpandProperty ServerAddresses)
            ipv4_enabled    = $null -ne $ipv4
            ipv4_forwarding = if ($ipv4) {{ $ipv4.Forwarding -eq 1 }} else {{ $null }}
            ipv4_gateways   = Get-Gateways -family IPv4
            ipv4_metric     = if($ipv4) {{ $ipv4.InterfaceMetric }} else {{ 0 }}
            ipv4_netbios    = switch($ipv4.NetbiosSetting) {{ 1 {{"Enable"}} 2 {{"Disable"}} Default {{"Default"}} }}
            ipv4_wins       = @($wmi.WINSPrimaryServer, $wmi.WINSSecondaryServer).Where({{$_}})

            # IPv6 Stack
            ipv6_address    = Get-CIDR -family IPv6
            ipv6_dhcp       = if($ipv6) {{ $ipv6.Dhcp -eq 1 }} else {{ $false }}
            ipv6_dns        = @($dns | Where-Object {{$_.AddressFamily -eq 23}} | Select-Object -ExpandProperty ServerAddresses)
            ipv6_enabled    = $null -ne $ipv6
            ipv6_forwarding = if ($ipv6) {{ $ipv6.Forwarding -eq 1 }} else {{ $null }}
            ipv6_gateways   = Get-Gateways -family IPv6
            ipv6_metric     = if($ipv6) {{ $ipv6.InterfaceMetric }} else {{ 0 }}
        }}

        $result | ConvertTo-Json -Depth 5 -Compress
        """

    with salt.utils.win_pwsh.PowerShellSession() as session:
        # Load modules once per session for efficiency
        session.import_modules(["NetAdapter", "NetTCPIP", "DnsClient"])
        data = session.run_json(cmd)

    if data:
        _normalize_gateway_fields(data)

    return {iface: data}


def list_interfaces(full=False):
    """
    Lists the available network interfaces on the system.

    Args:

        full (bool, optional): If ``True``, returns a dictionary keyed by
            interface names with detailed configuration for each adapter.
            If ``False``, returns a list of interface names only.
            Defaults to ``False``.

    Returns:
        list: When ``full=False``, a list of interface name strings.

        dict: When ``full=True``, a dictionary keyed by interface name.
            Each value has the same structure as the per-interface dict
            returned by :func:`get_interface_new` — see that function for
            the full field reference (``alias``, ``description``, ``enabled``,
            ``index``, ``link_status``, ``mac_address``, ``mtu``,
            ``dns_register``, ``dns_suffix``, ``ipv4_*``, ``ipv6_*``).

            The entire dataset is collected in a **single PowerShell
            invocation** rather than one session per adapter, so it is
            significantly faster than calling :func:`get_interface_new`
            in a loop.

    CLI Example:

    .. code-block:: bash

        salt -G 'os_family:Windows' ip.list_interfaces
        salt -G 'os_family:Windows' ip.list_interfaces full=True
    """
    with salt.utils.win_pwsh.PowerShellSession() as session:
        session.import_modules(["NetAdapter", "NetTCPIP", "DnsClient"])

        if not full:
            data = session.run_json("(Get-NetAdapter).Name")
            if not data:
                return []
            return data if isinstance(data, list) else [data]

        # Gather all adapter details in a single PowerShell invocation to avoid
        # opening N separate sessions (one per adapter).
        cmd = r"""
            function Get-CIDR {
                param([int]$idx, [string]$family)
                $ips = Get-NetIPAddress -InterfaceIndex $idx -AddressFamily $family -ErrorAction SilentlyContinue |
                       Where-Object { $_.AddressState -eq 'Preferred' -and $_.IPAddress -notlike 'fe80*' }
                if ($ips) { @($ips | ForEach-Object { "$($_.IPAddress)/$($_.PrefixLength)" }) } else { @() }
            }

            function Get-Gateways {
                param([int]$idx, [string]$family)
                $prefix = if ($family -eq 'IPv4') { "0.0.0.0/0" } else { "::/0" }
                $routes = Get-NetRoute -InterfaceIndex $idx -DestinationPrefix $prefix -ErrorAction SilentlyContinue |
                          Where-Object { $_.NextHop -and $_.NextHop -notmatch '^0\.0\.0\.0$|^::$' }
                if ($routes) {
                    @($routes | Select-Object @{Name='ip';Expression={$_.NextHop}}, @{Name='metric';Expression={$_.RouteMetric}})
                } else { @() }
            }

            $out = @{}
            Get-NetAdapter | ForEach-Object {
                $adapter = $_
                $idx     = [int]$adapter.ifIndex

                $ipv4 = Get-NetIPInterface -InterfaceIndex $idx -AddressFamily IPv4 -ErrorAction SilentlyContinue
                $ipv6 = Get-NetIPInterface -InterfaceIndex $idx -AddressFamily IPv6 -ErrorAction SilentlyContinue
                $dns  = Get-DnsClientServerAddress -InterfaceIndex $idx -ErrorAction SilentlyContinue
                $reg  = Get-DnsClient -InterfaceIndex $idx -ErrorAction SilentlyContinue
                $wmi  = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "InterfaceIndex = $idx" -ErrorAction SilentlyContinue

                $rawMtu   = if ($ipv4) { $ipv4.NlMtu } elseif ($ipv6) { $ipv6.NlMtu } else { $adapter.MtuSize }
                $finalMtu = if ($rawMtu -eq 4294967295 -or $null -eq $rawMtu) { 1500 } else { $rawMtu }

                $out[$adapter.Name] = [PSCustomObject]@{
                    alias           = $adapter.Name
                    description     = $adapter.InterfaceDescription
                    dns_register    = $reg.RegisterThisConnectionsAddress
                    dns_suffix      = $reg.ConnectionSpecificSuffix
                    enabled         = $adapter.AdminStatus -eq 1
                    index           = $idx
                    link_status     = $adapter.Status
                    mac_address     = $adapter.MacAddress
                    mtu             = [int]$finalMtu

                    ipv4_address    = Get-CIDR -idx $idx -family IPv4
                    ipv4_dhcp       = if ($ipv4) { $ipv4.Dhcp -eq 1 } else { $false }
                    ipv4_dns        = @($dns | Where-Object { $_.AddressFamily -eq 2 } | Select-Object -ExpandProperty ServerAddresses)
                    ipv4_enabled    = $null -ne $ipv4
                    ipv4_forwarding = if ($ipv4) { $ipv4.Forwarding -eq 1 } else { $null }
                    ipv4_gateways   = Get-Gateways -idx $idx -family IPv4
                    ipv4_metric     = if ($ipv4) { $ipv4.InterfaceMetric } else { 0 }
                    ipv4_netbios    = switch ($ipv4.NetbiosSetting) { 1 { "Enable" } 2 { "Disable" } Default { "Default" } }
                    ipv4_wins       = @($wmi.WINSPrimaryServer, $wmi.WINSSecondaryServer).Where({ $_ })

                    ipv6_address    = Get-CIDR -idx $idx -family IPv6
                    ipv6_dhcp       = if ($ipv6) { $ipv6.Dhcp -eq 1 } else { $false }
                    ipv6_dns        = @($dns | Where-Object { $_.AddressFamily -eq 23 } | Select-Object -ExpandProperty ServerAddresses)
                    ipv6_enabled    = $null -ne $ipv6
                    ipv6_forwarding = if ($ipv6) { $ipv6.Forwarding -eq 1 } else { $null }
                    ipv6_gateways   = Get-Gateways -idx $idx -family IPv6
                    ipv6_metric     = if ($ipv6) { $ipv6.InterfaceMetric } else { 0 }
                }
            }
            $out | ConvertTo-Json -Depth 5 -Compress
        """
        data = session.run_json(cmd)
        if not data:
            return {}
        for adapter_data in data.values():
            if isinstance(adapter_data, dict):
                _normalize_gateway_fields(adapter_data)
        return data