Skip to content

Commit

Permalink
HyperV platform port forwarding for internal switches
Browse files Browse the repository at this point in the history
Virtual machines with an internal switch type cannot be accessed from outside the host. To enable external access, you need to use NAT and port mapping.
  • Loading branch information
SRIKKANTH committed Feb 2, 2025
1 parent f850c60 commit 096d8a2
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 6 deletions.
12 changes: 11 additions & 1 deletion lisa/sut_orchestrator/hyperv/platform_.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from lisa.node import RemoteNode
from lisa.platform_ import Platform
from lisa.tools import Cp, HyperV, Mkdir, Mount, PowerShell
from lisa.tools.hyperv import HypervSwitchType
from lisa.util import LisaException, constants
from lisa.util.logger import Logger, get_logger
from lisa.util.parallel import run_in_parallel
Expand Down Expand Up @@ -309,10 +310,19 @@ def _deploy_environment(self, environment: Environment, log: Logger) -> None:
hv.start_vm(name=vm_name, extra_args=extra_args)

ip_addr = hv.get_ip_address(vm_name)
port = 22
# If the switch type is internal, we need to add a NAT mapping to access the
# VM from the outside of HyperV host.
if default_switch.type == HypervSwitchType.INTERNAL:
port = hv.add_nat_mapping(
nat_name=default_switch.name,
internal_ip=ip_addr,
)
ip_addr = node_context.host.public_address
username = self.runbook.admin_username
password = self.runbook.admin_password
node.set_connection_info(
address=ip_addr, username=username, password=password
address=ip_addr, username=username, password=password, public_port=port
)
# In some cases, we observe that resize vhd resizes the entire disk
# but fails to expand the partition size.
Expand Down
88 changes: 83 additions & 5 deletions lisa/tools/hyperv.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import time
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Optional
from typing import Any, Dict, Optional, Set

from assertpy import assert_that
from dataclasses_json import dataclass_json
Expand All @@ -32,9 +32,18 @@ class VMSwitch:


class HyperV(Tool):
# Internal NAT network configuration
INTERNAL_NAT_ROUTER = "192.168.5.1"
INTERNAL_NAT_SUBNET = "192.168.5.0/24"
INTERNAL_NAT_DHCP_IP_START = "192.168.5.50"
INTERNAL_NAT_DHCP_IP_END = "192.168.5.100"
# Azure DNS server
AZURE_DNS_SERVER = "168.63.129.16"
# 192.168.5.12
IP_REGEX = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
_default_switch: Optional[VMSwitch] = None
_external_forwarding_port_start = 50000
_assigned_nat_ports: Set[int] = set()

@property
def command(self) -> str:
Expand All @@ -58,6 +67,10 @@ def delete_vm_async(self, name: str) -> Optional[Process]:
if not self.exists_vm(name):
return None

# delete port mapping for internal IP address of the VM
internal_ip = self.get_ip_address(name)
self.delete_nat_mapping(internal_ip=internal_ip)

# stop and delete vm
self.stop_vm(name=name)
powershell = self.node.tools[PowerShell]
Expand Down Expand Up @@ -293,6 +306,50 @@ def create_nat(self, name: str, ip_range: str) -> None:
force_run=True,
)

def add_nat_mapping(self, nat_name: str, internal_ip: str) -> int:
# get next available NAT port
external_port = self._get_next_available_nat_port()
# create a new NAT
self.node.tools[PowerShell].run_cmdlet(
f"Add-NetNatStaticMapping -NatName {nat_name} -Protocol TCP "
f"-ExternalIPAddress 0.0.0.0 -InternalIPAddress {internal_ip} "
f"-InternalPort 22 -ExternalPort {external_port}",
force_run=True,
)
return external_port

def get_nat_mapping_id(self, external_port: int) -> Any:
return self.node.tools[PowerShell].run_cmdlet(
f"Get-NetNatStaticMapping | "
f"Where-Object {{$_.ExternalPort -eq {external_port}}}"
f" | Select-Object -ExpandProperty StaticMappingID",
force_run=True,
)

# delete NAT mapping for a port or internal IP
def delete_nat_mapping(
self, external_port: Optional[int] = None, internal_ip: Optional[str] = None
) -> None:
if external_port is None:
external_port = self.node.tools[PowerShell].run_cmdlet(
f"Get-NetNatStaticMapping | "
f"Where-Object {{$_.InternalIPAddress -eq '{internal_ip}'}}"
f" | Select-Object -ExpandProperty ExternalPort",
force_run=True,
)
mapping_id = self.get_nat_mapping_id(external_port)
if mapping_id:
# delete the NAT mapping if it exists
self.node.tools[PowerShell].run_cmdlet(
f"Remove-NetNatStaticMapping -StaticMappingID {mapping_id} "
"-Confirm:$false",
force_run=True,
)
if external_port:
self._release_nat_port(int(external_port))
else:
self._log.debug(f"Mapping for port {external_port} does not exist")

def delete_nat_networking(self, switch_name: str, nat_name: str) -> None:
# Delete switch
self.delete_switch(switch_name)
Expand All @@ -313,13 +370,13 @@ def setup_nat_networking(self, switch_name: str, nat_name: str) -> None:

# set switch interface as gateway for NAT
self.node.tools[PowerShell].run_cmdlet(
"New-NetIPAddress -IPAddress 192.168.5.1 "
f"New-NetIPAddress -IPAddress {self.INTERNAL_NAT_ROUTER} "
f"-InterfaceIndex {interface_index} -PrefixLength 24",
force_run=True,
)

# create a new NAT
self.create_nat(nat_name, "192.168.5.0/24")
self.create_nat(nat_name, self.INTERNAL_NAT_SUBNET)

def get_ip_address(self, name: str) -> str:
# verify vm is running
Expand Down Expand Up @@ -432,13 +489,18 @@ def enable_internal_dhcp(self, dhcp_scope_name: str = "DHCPInternalNAT") -> None

# Configure the DHCP server to use the internal NAT network
powershell.run_cmdlet(
f'Add-DhcpServerV4Scope -Name "{dhcp_scope_name}" -StartRange 192.168.0.50 -EndRange 192.168.0.100 -SubnetMask 255.255.255.0', # noqa: E501
f'Add-DhcpServerV4Scope -Name "{dhcp_scope_name}" '
f"-StartRange {self.INTERNAL_NAT_DHCP_IP_START} "
f"-EndRange {self.INTERNAL_NAT_DHCP_IP_END} "
f"-SubnetMask 255.255.255.0",
force_run=True,
)

# Set the DHCP server options
powershell.run_cmdlet(
"Set-DhcpServerV4OptionValue -Router 192.168.0.1 -DnsServer 168.63.129.16",
f"Set-DhcpServerV4OptionValue "
f"-Router {self.INTERNAL_NAT_ROUTER} "
f"-DnsServer {self.AZURE_DNS_SERVER}",
force_run=True,
)

Expand Down Expand Up @@ -482,3 +544,19 @@ def _run_hyperv_cmdlet(

def _check_exists(self) -> bool:
return self.node.tools[WindowsFeatureManagement].is_installed("Hyper-V")

def _get_next_available_nat_port(self) -> int:
# Start checking from the external forwarding port start and
# find the first available one
port = self._external_forwarding_port_start
while self.get_nat_mapping_id(port):
port += 1
self._assigned_nat_ports.add(port) # Assign this port
return port

def _release_nat_port(self, port: int) -> None:
# Release a port if used
if port in self._assigned_nat_ports:
self._assigned_nat_ports.remove(port)
else:
print(f"Port {port} was not assigned.")

0 comments on commit 096d8a2

Please sign in to comment.