Public/ContainerNetworkTools.psm1

###########################################################################
# #
# Copyright (c) Microsoft Corporation. All rights reserved. #
# #
# This code is licensed under the MIT License (MIT). #
# #
###########################################################################


using module "..\Private\CommonToolUtilities.psm1"

$ModuleParentPath = Split-Path -Parent $PSScriptRoot
Import-Module -Name "$ModuleParentPath\Private\CommonToolUtilities.psm1" -Force

function Get-WinCNILatestVersion {
    param (
        [String]$repo = "microsoft/windows-container-networking"
    )
    $tool = switch ($repo.ToLower()) {
        $WINCNI_PLUGIN_REPO { "wincniplugin" }
        $CLOUDNATIVE_CNI_REPO { "cloudnativecni" }
        Default { Throw "Invalid repository. Supported repositories are $WINCNI_PLUGIN_REPO and $CLOUDNATIVE_CNI_REPO" }
    }
    $latestVersion = Get-LatestToolVersion -Tool $tool
    return $latestVersion
}

function Install-WinCNIPlugin {
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High'
    )]
    param(
        [parameter(HelpMessage = "Windows CNI plugin version to use. Defaults to 'latest'")]
        [string]$WinCNIVersion = "latest",

        [parameter(HelpMessage = "Path to cni folder ~\cni (not ~\cni\bin). Defaults to `$env:ProgramFiles\containerd\cni)")]
        [String]$WinCNIPath = "$env:ProgramFiles\containerd\cni",

        [parameter(HelpMessage = "Source of the Windows CNI plugins. Defaults to 'microsoft/windows-container-networking'")]
        [ValidateSet("microsoft/windows-container-networking", "containernetworking/plugins")]
        [string]$SourceRepo = "microsoft/windows-container-networking",

        [Parameter(HelpMessage = 'OS architecture to download files for. Default is $env:PROCESSOR_ARCHITECTURE')]
        [ValidateSet('amd64', '386', "arm", "arm64")]
        [string]$OSArchitecture = $env:PROCESSOR_ARCHITECTURE,

        [Switch]
        [parameter(HelpMessage = "Installs Windows CNI plugins even if the tool already exists at the specified path")]
        $Force
    )

    begin {
        $ToolName = 'WinCNIPlugin'
        if (!$WinCNIPath) {
            $containerdPath = Get-DefaultInstallPath -Tool "containerd"
            $WinCNIPath = "$containerdPath\cni"
        }
        $WinCNIPath = $WinCNIPath -replace '(\\bin)$', ''

        # Check if WinCNI plugins are installed
        $isInstalled = -not (Test-EmptyDirectory -Path $WinCNIPath)

        $plugin = "Windows CNI plugins"

        $WhatIfMessage = "$plugin will be installed at $WINCNIPath"
        if ($isInstalled) {
            $WhatIfMessage = "$plugin will be uninstalled from and reinstalled at $WINCNIPath"
        }
    }

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $WhatIfMessage)) {
            # Check if tool already exists at specified location
            if ($isInstalled) {
                $errMsg = "Windows CNI plugins already exists at $WinCNIPath or the directory is not empty"
                Write-Warning $errMsg

                # Uninstall if tool exists at specified location. Requires user consent
                try {
                    Uninstall-WinCNIPlugin -Path "$WinCNIPath" -Confirm:$false -Force:$Force | Out-Null
                }
                catch {
                    Throw "Windows CNI plugin installation failed. $_"
                }
            }

            # Get Windows CNI plugins version to install
            if (!$WinCNIVersion) {
                # Get default version
                $WinCNIVersion = Get-WinCNILatestVersion -Repo $SourceRepo
            }
            $WinCNIVersion = $WinCNIVersion.TrimStart('v')
            Write-Output "Downloading CNI plugin version $WinCNIVersion at $WinCNIPath"

            New-Item -Path $WinCNIPath -ItemType Directory -Force -ErrorAction Ignore | Out-Null

            Write-Debug ("Downloading Windows CNI plugins from {0}" -f $SourceRepo)

            # File filter for Windows CNI plugins
            $fileFilterRegEx = $null

            if ($SourceRepo -eq "containernetworking/plugins") {
                # File filter for containernetworking/plugins
                # Contains files with .tgz extension and the checksum files with .SHA512 and .SHA256 extensions
                # We use .SHA512 files to verify the integrity of the downloaded files
                $fileFilterRegEx = ".*tgz(.SHA512)?$"
            }

            # Download file from repo
            $downloadParams = @{
                ToolName           = "$ToolName"
                Repository         = $SourceRepo
                Version            = $WinCNIVersion
                OSArchitecture     = $OSArchitecture
                DownloadPath       = "$HOME\Downloads\"
                ChecksumSchemaFile = $null
                FileFilterRegEx    = $fileFilterRegEx
            }
            $downloadParamsProperties = [FileDownloadParameters]::new(
                $downloadParams.ToolName,
                $downloadParams.Repository,
                $downloadParams.Version,
                $downloadParams.OSArchitecture,
                $downloadParams.DownloadPath,
                $downloadParams.ChecksumSchemaFile,
                $downloadParams.FileFilterRegEx
            )
            $sourceFile = Get-InstallationFile -FileParameters $downloadParamsProperties

            # Untar and install tool
            $params = @{
                Feature       = "$ToolName"
                InstallPath   = "$WinCNIPath\bin"
                SourceFile    = $sourceFile
                cleanup       = $true
                UpdateEnvPath = $false
            }
            Install-RequiredFeature @params

            Write-Output "CNI plugin version $WinCNIVersion ($sourceRepo) successfully installed at $WinCNIPath"
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

function Initialize-NatNetwork {
    [CmdletBinding(
        SupportsShouldProcess = $true
    )]
    param(
        [parameter(HelpMessage = "Name of the new network. Defaults to 'nat''")]
        [String]$NetworkName = "nat",

        [parameter(HelpMessage = "Gateway IP address. Defaults to default gateway address'")]
        [String]$Gateway,

        [parameter(HelpMessage = "Size of the subnet mask. Defaults to 16")]
        [ValidateRange(0, 32)]
        [Int]$CIDR = 16,

        [parameter(HelpMessage = "Windows CNI plugins version to use. Defaults to latest version.")]
        [String]$WinCNIVersion,

        [parameter(HelpMessage = "Absolute path to cni folder ~\cni (not ~\cni\bin). Defaults to `$env:ProgramFiles\containerd\cni)")]
        [String]$WinCNIPath = "$env:ProgramFiles\containerd\cni",

        [parameter(HelpMessage = "Bypass confirmation to install any missing dependencies (Windows CNI plugins and HNS module)")]
        [Switch] $Force
    )

    begin {
        if (!$WinCNIPath) {
            $ContainerdPath = Get-DefaultInstallPath -Tool "containerd"
            $WinCNIPath = "$ContainerdPath\cni"
        }
        $WinCNIPath = $WinCNIPath -replace '(\\bin)$', ''
        $cniConfDir = "$WinCNIPath\conf"

        # Check if WinCNI plugins is already installed
        $isInstalled = -not (Test-EmptyDirectory -Path "$WinCNIPath\bin")

        $WhatIfMessage = "Initialises a NAT network using Windows CNI plugins installed"
        if (!$isInstalled) {
            $WhatIfMessage = "`n`t1. Import `"HostNetworkingService`" or `"HNS`" module,`n`t2. Install Windows CNI plugins, and 3. Initialize a NAT network using Windows CNI plugins installed`n"
        }
    }

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $WhatIfMessage)) {
            if (!$force) {
                if (!$ENV:PESTER) {
                    if (-not $PSCmdlet.ShouldContinue('', "Are you sure you want to initialises a NAT network?`n`t`tHNS module will be imported and missing dependencies (Windows CNI Plugins) will be installed if missing.")) {
                        Write-Error "NAT network initialisation cancelled."
                        return
                    }
                }
            }

            # Import HNS module
            try {
                Import-HNSModule -Force:$Force
            }
            catch {
                Throw "Could not import HNS module. $_"
            }

            Write-Information -MessageData "Creating NAT network" -InformationAction Continue

            # Install missing WinCNI plugins
            if (!$isInstalled) {
                if ($force) {
                    Write-Warning "Windows CNI plugins have not been installed. Installing Windows CNI plugins at '$WinCNIPath'"
                    Install-WinCNIPlugin -WinCNIPath $WinCNIPath -WinCNIVersion $WinCNIVersion -Force:$force
                }
                else {
                    Write-Warning "Couldn't initialize NAT network. CNI plugins have not been installed. To install, run the command `"Install-WinCNIPlugin`"."
                    return
                }
            }

            # Check of NAT exists
            $natInfo = Get-HnsNetwork -ErrorAction Ignore | Where-Object { $_.Name -eq $networkName }
            if ($null -ne $natInfo) {
                Write-Warning "$networkName already exists. To view existing networks, use `"Get-HnsNetwork`". To remove the existing network use the `"Remove-HNSNetwork`" command."
                return
            }

            New-Item -ItemType 'Directory' -Path $cniConfDir -Force | Out-Null

            # Check if `New-HNSNetwork` command exists
            if (-not (Get-Command -Name 'New-HNSNetwork' -ErrorAction SilentlyContinue)) {
                Throw "`"New-HNSNetwork`" command does not exist. Ensure the HNS module is installed. To resolve this issue, see`n`thttps://github.com/microsoft/containers-toolkit/blob/main/docs/FAQs.md#2-new-hnsnetwork-command-does-not-exist"
            }

            # Set default gateway if gateway us null and generate subnet mash=k from Gateway
            if (!$gateway) {
                $gateway = (Get-NetRoute -DestinationPrefix "0.0.0.0/0").NextHop
            }
            $networkIdentifier = $gateway -replace "\.\d*$", ".0"
            $subnet = "$networkIdentifier/$CIDR"

            Write-Debug "Creating NAT network with Gateway $gateway and Subnet mask $subnet"

            # Set default WinCNI version of null
            if (!$WinCNIVersion) {
                # Get default version
                $WinCNIVersion = Get-WinCNILatestVersion
            }
            $WinCNIVersion = $WinCNIVersion.TrimStart('v')

            try {
                # Restart HNS service
                Get-Service "hns" -ErrorAction SilentlyContinue | Restart-Service -Force -ErrorAction SilentlyContinue

                # Create NAT network
                $hnsNetwork = New-HNSNetwork -Name $networkName -Type NAT -AddressPrefix $subnet -Gateway $gateway

                # Set default CNI config
                $params = @{
                    WinCNIVersion = $WinCNIVersion
                    NetworkName   = $networkName
                    Gateway       = $gateway
                    Subnet        = $subnet
                    CNIConfDir    = $cniConfDir
                }
                Set-DefaultCNIConfig @params

                Write-Output "Successfully created new NAT network called '$($hnsNetwork.Name)' with Gateway $($hnsNetwork.Subnets.GatewayAddress), and Subnet Mask $($hnsNetwork.Subnets.AddressPrefix)"
            }
            catch {
                Throw "Could not create a new NAT network $networkName with Gateway $gateway and Subnet mask $subnet. $_"
            }
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function/script
            return
        }
    }
}

function Uninstall-WinCNIPlugin {
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High'
    )]
    param(
        [parameter(HelpMessage = "Windows CNI plugin path")]
        [String]$Path,

        [parameter(HelpMessage = "Bypass confirmation to uninstall Windows CNI plugins")]
        [Switch] $Force
    )

    begin {
        $tool = 'WinCNIPlugin'

        if (!$Path) {
            $ContainerdPath = Get-DefaultInstallPath -Tool "containerd"
            $Path = "$ContainerdPath\cni"
        }

        $Path = $Path -replace '(\\bin\\?)$', ''

        $WhatIfMessage = "Windows CNI plugins will be uninstalled from $path"
    }

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $WhatIfMessage)) {
            if (Test-EmptyDirectory -Path $path) {
                Write-Output "Windows CNI plugins does not exist at $Path or the directory is empty"
                return
            }

            # Check user consents to uninstall WinCNIPlugin
            $consent = $force
            if (!$ENV:PESTER) {
                $consent = $force -or $PSCmdlet.ShouldContinue($Path, 'Are you sure you want to uninstall Windows CNI plugins?')
            }

            if (!$consent) {
                Throw "Windows CNI plugins uninstallation cancelled."
            }

            Write-Warning "Uninstalling preinstalled Windows CNI plugin at the path $path"
            try {
                Uninstall-WinCNIPluginHelper -Path $path
            }
            catch {
                Throw "Could not uninstall $tool. $_"
            }
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

function Uninstall-WinCNIPluginHelper {
    param(
        [ValidateNotNullOrEmpty()]
        [parameter(HelpMessage = "Windows CNI plugin path")]
        [String]$Path
    )

    Write-Output "Uninstalling Windows CNI plugin"
    if (Test-EmptyDirectory -Path $Path) {
        Write-Error "Windows CNI plugin does not exist at $Path or the directory is empty."
        return
    }

    # Remove the folder where WinCNI plugins are installed
    Remove-Item $Path -Recurse -Force -ErrorAction Ignore

    Write-Output "Successfully uninstalled Windows CNI plugin."
}

function Import-HNSModule {
    param(
        [Switch] $Force
    )

    $ModuleName = 'HostNetworkingService'
    # https://learn.microsoft.com/en-us/powershell/module/hostnetworkingservice/?view=windowsserver2025-ps
    if ((Get-Module -Name $ModuleName)) {
        return
    }

    $ModuleName = 'HNS'
    # https://www.powershellgallery.com/packages/HNS/0.2.4
    if (Get-Module -ListAvailable -Name $ModuleName) {
        Import-Module -Name $ModuleName -DisableNameChecking -Force:$Force
        return
    }

    # Throw an error if the module is not installed
    Throw "`"HostNetworkingService`" or `"HNS`" module is not installed. To resolve this issue, see`n`thttps://github.com/microsoft/containers-toolkit/blob/main/docs/FAQs.md#2-new-hnsnetwork-command-does-not-exist"
}

# FIXME: nerdctl- Warning when user tries to run container with this network config
function Set-DefaultCNIConfig {
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Low'
    )]
    param(
        [String]$WinCNIVersion,
        [String]$networkName,
        [String]$gateway,
        [String]$subnet,
        [String]$cniConfDir
    )

    process {
        if ($PSCmdlet.ShouldProcess('', "Sets Default CNI config")) {
            # TODO: Default CNI config for containernetworking/plugins
            # https://www.cni.dev/plugins/current/main/win-bridge/
            # https://www.cni.dev/plugins/current/main/win-overlay/
            $CNIConfig = @"
{
"cniVersion": "$WinCNIVersion",
"name": "$networkName",
"type": "nat",
"master": "Ethernet",
"ipam": {
    "subnet": "$subnet",
    "routes": [
        {
            "gateway": "$gateway"
        }
    ]
},
"capabilities": {
    "portMappings": true,
    "dns": true
    }
}
"@

            $CNIConfig | Set-Content "$cniConfDir\0-containerd-nat.conf" -Force
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

Export-ModuleMember -Function Get-WinCNILatestVersion
Export-ModuleMember -Function Install-WinCNIPlugin
Export-ModuleMember -Function Uninstall-WinCNIPlugin, Uninstall-WinCNIPluginHelper
Export-ModuleMember -Function Initialize-NatNetwork

# SIG # Begin signature block
# MIIoXQYJKoZIhvcNAQcCoIIoTjCCKEoCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCrz2Q+H9PcrRPn
# QajuhQk9n5MNAIe077+VpVpoJuzh06CCDYswggYJMIID8aADAgECAhMzAAAD9LjE
# XeFOcLZ+AAAAAAP0MA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwNzE3MjEwMjM1WhcNMjUwOTE1MjEwMjM1WjCBiDEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
# bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWlj
# cm9zb2Z0IDNyZCBQYXJ0eSBBcHBsaWNhdGlvbiBDb21wb25lbnQwggEiMA0GCSqG
# SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv3P8bL08GKolFW7QNDVOF0aM4iqMxVvAW
# VM124/82xbjAraJkKxieMrQa1Fc95LVGgxmJIi5R6QKMz2MO9bnwC7kSkPqoZJil
# 26bRLY6jinjbwPpK3TzbW7z9bXfWw5bPFlt72NVIdXJ3xtHoYa+AOi++CF2Ry7+7
# o1AzvotJwG6lQSiCMKeMt8apqEF1f+QkDFEUv5tezw9748DeHW9orvo4IPzWa7vW
# QgljB08LKSnzTN9/Jot2coWpFv4YuEoJZmR2ofPJMnDUUruDORTXnxwhfvd/wUmI
# SoEysSqobkNV+qFuUmSShYrx8R1zHm7P6G/iRMIKYmSrIYBKUvndAgMBAAGjggFz
# MIIBbzAfBgNVHSUEGDAWBgorBgEEAYI3TBEBBggrBgEFBQcDAzAdBgNVHQ4EFgQU
# Dz4uMjS8YCSZaU0449GJYQ1ufyowRQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEWMBQGA1UEBRMNMjMxNTIyKzUwMjUxODAfBgNV
# HSMEGDAWgBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNo
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0Ey
# MDExXzIwMTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZF
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQ
# Q0EyMDExXzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL
# BQADggIBABUCAiEn4g8i5T3VCP8160IY4ERdvZi5QZ2pSnBPW1dswVhLxkNTiCTV
# XKDjTQ4EwDBNSZZGJePz4+t86pKhlBON3S7wswf5fCovJLlIiKbw+E4TZeY6xAxd
# +5zV7Q2lsQhPHxiOY0PIGUE0KJfv/DQUulD8DrE0rru7yOO+DJI0muoK0BbHhRfd
# mAJhp2gbYRkarEIkhML9m3gR12mCBb69Vocm4IyOBivUPMjjvQMkERF7cR07k2uP
# 6dmpR8wtof9la0/K0wgiP5XuQUsAqgzhXrljH7dK7nqGrBDjJtrRdYfvVL+Rcz9i
# YZO280g2uNtac5em3HOEsactAL7XKqZ4o7s9sRyp/bTNLLRmhFMB729IL+Hi0YM7
# C8th3HZ5nP+77L46KUGip6QgRIJs+EO0YNW+AwgMxPfKpTx/Ggh8Z85kP7HLDZJk
# ZdPO/3cgVOTO4ax21vO2yMPCdfoGGr2ZLZw4SjEbGuOZJ22iGMV7tBvHk8nWAt3q
# +j/icAq99GA1nIPnw3jK3K9OwGqwA9eiWsO8/bHMm6s50UKIFupMKm6qObosaVBy
# R58rf8Cxumka7hPy1eSJSzQyA4UqYNTWuChsTfqgRLmLomS6yAu7t4r/bM4mGl+2
# Ki+avhQ4COm3jWWd0V6UGIP3T4zaKNs2GWFBIYsb/6XVvvi7pz/JMIIHejCCBWKg
# AwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3Qg
# Q2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYw
# NzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ
# MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
# MSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjAN
# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGf
# Qhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRg
# JGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NE
# t13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnn
# Db6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+E
# GvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+t
# GSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxh
# H2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AV
# s70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f
# 7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3D
# KI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9Jaw
# vEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1Ud
# DgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBi
# AEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRy
# LToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3Js
# Lm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDEx
# XzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDEx
# XzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/
# BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2Nz
# L3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAA
# bwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUA
# A4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+
# vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4H
# Limb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6
# aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiX
# mE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn
# +N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAq
# ZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5h
# YbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/
# RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXm
# r/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMyk
# XcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGigwghokAgEB
# MIGVMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNV
# BAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAP0uMRd4U5w
# tn4AAAAAA/QwDQYJYIZIAWUDBAIBBQCggbAwGQYJKoZIhvcNAQkDMQwGCisGAQQB
# gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE
# MSIEIAlf9KwVTG6mlXPpTwtNj4NZzWkRo3BH9aRZa/iqspdvMEQGCisGAQQBgjcC
# AQwxNjA0oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEcgBpodHRwczovL3d3dy5taWNy
# b3NvZnQuY29tIDANBgkqhkiG9w0BAQEFAASCAQClrflgU6qjBaf1lrT38IvKnDdZ
# a/Y1ke7bHBERD72zpazUuLRvAFeg1Ol+1cRygCWL3LXilKGXUM6h7yGOMNNgDVgP
# GCqW2WnzxsE+mkLOjh8EGvtZoq4yO0U+ruqVpmZFO9NS8Fto0mBGRk0OCEUzztT8
# fJ0PlNJpfN22K+ZhLOH4Y/ldEf5hGpt03VdvStcQ3QxG9q9GnYVLLWpIHyFkB3fi
# C+KT2z3qsaJevfhbFUyhJU0zSWU0NlWumXQRqUCCl1lP3o47uzPVHYPl6SGNkaY+
# Mh/qNDQ952UsmJQLOv+WzSMRO2QRqcdPfaov7/JJyvaea20CLvUyeGVFO7IboYIX
# sDCCF6wGCisGAQQBgjcDAwExghecMIIXmAYJKoZIhvcNAQcCoIIXiTCCF4UCAQMx
# DzANBglghkgBZQMEAgEFADCCAVoGCyqGSIb3DQEJEAEEoIIBSQSCAUUwggFBAgEB
# BgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEINidRnHaNjiIB3Bz3cWsaZRW
# KjDYC8nspwelHFOqi/VOAgZoEsLQqJwYEzIwMjUwNTE1MTYxOTU4LjI3OVowBIAC
# AfSggdmkgdYwgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjJEMUEtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIR/jCCBygwggUQoAMCAQIC
# EzMAAAH9c/loWs0MYe0AAQAAAf0wDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwHhcNMjQwNzI1MTgzMTE2WhcNMjUxMDIyMTgzMTE2
# WjCB0zELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UE
# CxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMScwJQYDVQQL
# Ex5uU2hpZWxkIFRTUyBFU046MkQxQS0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQChZaz4P467gmNidEdF527QxMVjM0kRU+cgvNzTZHepue6O+FmCSGn6
# n+XKZgvORDIbbOnFhx5OMgXseJBZu3oVbcBGQGu2ElTPTlmcqlwXfWWlQRvyBReI
# PbEimjxgz5IPRL6FM/VMID/B7fzJncES2Zm1xWdotGn8C+yqD7kojQrDpMMmkrBM
# uXRVbT/bewqKR5YNKcdB5Oms7TMib9u1qBJibdX/zNeV/HLuz8RUV1KCUcaxSrwR
# m6lQ7xdsfPPu1RHKIPeQ7E2fDmjHV5lf9z9eZbgfpvjI2ZkXTBNm7DfvIDU8ko7J
# JKtetYSH4fr75Zvr7WW0wI+gwkdS08/cKfQI1w2+s/Im0NpyqOchOsvOuwd04uqO
# wfbb1mS+d2TQirEENmAyhj4R/t98VE/ak+SsXUX0hwGRjPyEv5CNf67jLhSqrhS1
# PtVGeyq9H/H/5AsTSlxISH9cTXDV9ynomarxGccReKTJwws39r8pjGlI/cV8Vstm
# 5/6oivIUvSAQPK1qkafU42NWSIqlU/a6pUhiPhWIKPLmktRx4x6qIqBiqGmZQcIT
# ZaywsuF1AEd2mXbz6T5ljqbh08WcSgZwke4xwhmfDhw7CLGiNE6v42rvVwmPtDgv
# RfA++5MdC3SgftEoxCCazLsJUPu/nl06F0dd1izI7r10B0r6daXJhwIDAQABo4IB
# STCCAUUwHQYDVR0OBBYEFOkMxcDhlbz7Ivb7e8DpGZTugQqkMB8GA1UdIwQYMBaA
# FJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRpbWUtU3Rh
# bXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYIKwYBBQUH
# MAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9z
# b2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQC
# MAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeAMA0GCSqG
# SIb3DQEBCwUAA4ICAQBj2Fhf5PkCYKtgZof3pN1HlPnb8804bvJJh6im/h+WcNZA
# uEGWtq8CD6mOU2/ldJdmsoa/x7izl0nlZ2F8L3LAVCrhOZedR689e2W5tmT7TYFc
# rr/beEzRNIqzYqWFiKrNtF7xBsx8pcQO28ygdJlPuv7AjYiCNhDCRr7c/1VeARHC
# 7jr9zPPwhH9mr687nnbcmV3qyxW7Oz27AismF9xgGPnSZdZEFwyHNqMuNYOByKHQ
# O7KQ9wGmhMuU4vwuleiiqev5AtgTgGlR6ncnJIxh8/PaF84veDTZYR+w7GnwA1tx
# 2KozfV2be9KF4SSaMcDbO4z5OCfiPmf4CfLsg4NhCQis1WEt0wvT167V0g+GnbiU
# W2dZNg1oVM58yoVrcBvwoMqJyanQC2FE1lWDQE8Avnz4HRRygEYrNL2OxzA5O7Um
# Y2WKw4qRVRWRInkWj9y18NI90JNVohdcXuXjSTVwz9fY7Ql0BL3tPvyViO3D8/Ju
# 7NfmyHEGH9GpM+8LICEjEFUp83+F+zgIigVqpYnSv/xIHUIazLIhw98SAyjxx6rX
# DlmjQl+fIWLoa6j7Pcs8WX97FBpG5sSuwBRN/IFjn/mWLK+MCDINicQHy8c7tzsW
# Da0Z3mEaBiz4A6hbHbj5dzLGlSQBqMOGTL0OX7wllOO2zoFxP2xhOY6h2T9KAjCC
# B3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQELBQAw
# gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMT
# KU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTIx
# MDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# UENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM57Ry
# IQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm95VT
# cVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzBRMhx
# XFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBbfowQ
# HJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCOMcg1
# KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYwXE8s
# 4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW/aUg
# fX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3
# Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je
# 1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUY
# hEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfHCBUY
# P3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYBBAGC
# NxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4w
# HQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYMKwYB
# BAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcD
# CDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0T
# AQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNV
# HR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9w
# cm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEE
# TjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2Nl
# cnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsFAAOC
# AgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518JxNj/a
# ZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+iehp
# 4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2pFaq
# 95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefwC2qB
# woEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG
# +jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFORy3B
# FARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77
# IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJ
# fn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5m/8K
# 6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDx
# yKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNZMIICQQIBATCC
# AQGhgdmkgdYwgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjJEMUEtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQCi
# PRa1VVBQ1Iqiq2uOKdECwFR2g6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA69BeITAiGA8yMDI1MDUxNTEyMzU0
# NVoYDzIwMjUwNTE2MTIzNTQ1WjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDr0F4h
# AgEAMAoCAQACAhfOAgH/MAcCAQACAhOKMAoCBQDr0a+hAgEAMDYGCisGAQQBhFkK
# BAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJ
# KoZIhvcNAQELBQADggEBAJR3JJeGI5QxaoQOYGTDMTcA+MnuvxP7mq902xY2eLwO
# cRPqnBeY3K85bzTLAiZRLJcREYOZIGlUTr6BCAjwBm0O5V05vcUL9pp/9GZ5HcJv
# kB+fU1micI7lnRwPaP3KoGUbvYnRZBiw3wn4fQC11Y0lxlqX9T0d/m+6PCz3SUf6
# R9qyUSj61Zu2oS3io0RlP+zaZXkO6WQ1/w9RKU4IesbrCnm1EiigoTy45MNvgvgc
# wvv6zUAz9XoCClXqlapLp5HPHD79lsoMvWWB2DYnyT40HvWCGO/+T4YAXU8tuNn/
# X4GQ7HJmWADc5Y8DfccqUFeGs1eTBF/DUHiETWQU86ExggQNMIIECQIBATCBkzB8
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N
# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAf1z+WhazQxh7QABAAAB
# /TANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEE
# MC8GCSqGSIb3DQEJBDEiBCA4vBiC3YTQ2lzDX+NFtPc3XWPZZWDWbSsWKDPI3tH6
# SzCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIIAoSA3JSjC8h/H94N9hK6ke
# R4XWnoYXCMoGzyVEHg1HMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAH9c/loWs0MYe0AAQAAAf0wIgQgGf/DtHulcoON6sk40jLMLWh7
# fw5JSCh5IriC2zp3jJwwDQYJKoZIhvcNAQELBQAEggIAZVwnOtckY7ruR2YRmTfz
# abQjwNJHyWky05sxfcd8Hop90cl6FJNwmHQhph8cDRYu108a5wG2RpVLBAdrIGkj
# uOhbBmOmPn+ka8PRre/7SLOelGhQvntTZGxVWJKLNi8gHU8kIQRL1F3F56qbnigz
# ewOwHvwvoz5b7mJ22mnQSOsrQbV+Say3Lp0cNW99bTbMyYr9IXOvBxhYDVdSpyVR
# dW5XF4ByAQ39Ifa0VHTi0O/62UwWJ/8cgS9pEHs6pZ4+vFmWfdYwgQIZA0ORJppq
# 5WmgC/WmSR6fF9fi2+ockDjumHQByXygsYEofF26IUNdjA2prObqYNXgpSvM5MMD
# FOp53IIGxJ8C4LLdZq5rjv3ODmnPACV1NPGO03fnsyYhMk4y15GUZJC9yNFifftb
# ssM9i5SkXUlDBLaEvZjeOQp941349ioMIDqDUZ7xFnZlnDkFS3/8EuQ0jUurUiED
# Lyf+PM+KUO82renkGcUxat7p257h8pVtxs07ne+obikKQizGExdJw9k6iwqbCPsy
# pyuRF5ceTMUOlVY3J+oemB+HoaIx8ewVxvA1mJzm440RB4Pm8T3A7A0tKvG6DHzV
# gNGwONXBwjSnPkCkp5Lp0HSWmPnPxdZvQzmtU1InsyD2MklDsdN+Wxc9Q3D4jLum
# eYsx4Aq0kjCSFrxSWwrbQtU=
# SIG # End signature block