modules/Test-SdnExpressBgp.psm1

# --------------------------------------------------------------
# Copyright © Microsoft Corporation. All Rights Reserved.
# Microsoft Corporation (or based on where you live, one of its affiliates) licenses this sample code for your internal testing purposes only.
# Microsoft provides the following sample code AS IS without warranty of any kind. The sample code arenot supported under any Microsoft standard support program or services.
# Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose.
# The entire risk arising out of the use or performance of the sample code remains with you.
# In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the code be liable for any damages whatsoever
# (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss)
# arising out of the use of or inability to use the sample code, even if Microsoft has been advised of the possibility of such damages.
# ---------------------------------------------------------------

# BGP-4 implementation

# RFCs:
# RFC 4271 BGP-4
# RFC 3392 Capabilities Advertisement with BGP-4
# RFC 2918 Route Refresh for BGP-4

# Slightly implemented (extra data shouldn't break):
# RFC 4760 Multiprotocol Extensions for BGP-4

# TODO:
# Detect RAS BGP, temporarily disable with -force.

# Usage Examples:
#
# computer already has NIC:
# Test-SdnExpressBGP -RouterIPAddress 10.10.182.3 -LocalIPAddress 10.10.182.7 -LocalASN 64628 -verbose -ComputerName sa18n22mux02
# computer does not have NIC:
# $h = New-SdnExpressBGPHost -computername sa18n22-2 -localipaddress "10.10.182.20" -prefixlength 26 -vlanid 11
# $h | Test-SdnExpressBGP -RouterIPAddress "10.10.182.3" -LocalASN 64628
# $h | Remove-SdnExpressBGPHost

function GetBGPPathAttributeType {
    param(
        [int] $code
    )
    if ($code -lt $BGP_PATH_ATTRIBUTE_TYPES.count) {
        return $BGP_PATH_ATTRIBUTE_TYPES[$code]
    }
    else {
        return "$code"
    }
}

function CapabilityCodeLookup {
    param(
        [int] $code
    )
    switch ($code) {
        0 { return "Reserved" }
        1 { return "Multiprotocol Extensions for BGP-4" }
        2 { return "Route Refresh Capability for BGP-4" }
        3 { return "Outbound Route Filtering Capability" }
        4 { return "Multiple routes to a destination capability (deprecated)" }
        5 { return "Extended Next Hop Encoding" }
        6 { return "BGP Extended Message" }
        7 { return "BGPsec Capability" }
        8 { return "Multiple Labels Capability" }
        9 { return "BGP Role (TEMPORARY)" }
        { $_ -in 10..63 } { return "Unassigned" }
        64 { return "Graceful Restart Capability" }
        65 { return "Support for 4-octet AS number capability" }
        66 { return "Deprecated" }
        67 { return "Support for Dynamic Capability (capability specific)" }
        68 { return "Multisession BGP Capability" }
        69 { return "ADD-PATH Capability" }
        70 { return "Enhanced Route Refresh Capability" }
        71 { return "Long-Lived Graceful Restart (LLGR) Capability" }
        72 { return "Routing Policy Distribution" }
        73 { return "FQDN Capability" }
        { $_ -in 74..127 } { return "Unassigned" }
        128 { return "Prestandard Route Refresh (deprecated)" }
        129 { return "Prestandard Outbound Route Filtering (deprecated)" }
        130 { return "Prestandard Outbound Route Filtering (deprecated)" }
        131 { return "Prestandard Multisession (deprecated)" }
        { $_ -in 132..183 } { return "Unassigned" }
        184 { return "Prestandard FQDN (deprecated)" }
        185 { return "Prestandard OPERATIONAL message (deprecated)" }
        { $_ -in 186..238 } { return "Unassigned" }
        { $_ -in 239..254 } { return "Reserved for Experimental Use" }
        255 { return "Reserved" }
    }

}

function GetBytes {
    param(
        [byte[]] $bytes,
        [int] $offset,
        [int] $count
    )
    return $bytes[$offset..($offset + $count - 1)]
}
function GetInt32 {
    param(
        [byte[]] $bytes,
        [int] $offset
    )
    return [System.Int64]($bytes[$offset] * [Math]::pow(2, 24)) + ($bytes[$offset + 1] * [Math]::pow(2, 16)) + ($bytes[$offset + 2] * [Math]::pow(2, 8)) + $bytes[$offset + 3]
}

function GetInt16 {
    param(
        [byte[]] $bytes,
        [int] $offset
    )
    return [Int]($bytes[$offset] * 256) + $bytes[$offset + 1]
}
function GetInt8 {
    param(
        [byte[]] $bytes,
        [int] $offset
    )
    return [Int]$bytes[$offset]
}
function SetInt8 {
    param(
        [byte[]] $bytes,
        [int] $offset,
        [int] $value
    )
    $bytes[$offset] = $value
    return $bytes
}
function SetInt16 {
    param(
        [byte[]] $bytes,
        [int] $offset,
        [int] $value
    )
    $bytes[$offset] = [byte](($value -shr 8) -band [Byte] 0xFF)
    $bytes[$offset + 1] = [byte]( $value -band [Byte] 0xFF)
    return $bytes
}
function SetInt32 {
    param(
        [byte[]] $bytes,
        [int] $offset,
        [int] $value
    )
    $bytes[$offset] = $value -band 0xFF
    $bytes[$offset + 1] = ($value -shr 8) -band 0xFF
    $bytes[$offset + 2] = ($value -shr 16) -band 0xFF
    $bytes[$offset + 3] = ($value -shr 24) -band 0xFF
    return $bytes
}
function AddInt8 {
    param(
        [byte[]] $bytes,
        [int] $value
    )
    $bytes += [byte] $value
    return $bytes
}
function AddInt16 {
    param(
        [byte[]] $bytes,
        [int] $value
    )
    $bytes += [byte] (($value -shr 8) -band [byte] 0xFF)
    $bytes += [byte] ($value -band [byte] 0xFF)
    return $bytes
}
function AddInt32 {
    param(
        [byte[]] $bytes,
        [System.Int64] $value
    )
    $bytes += [byte]($value -band [byte]0xFF)
    $bytes += [byte](($value -shr 8) -band [byte]0xFF)
    $bytes += [byte](($value -shr 16) -band [byte]0xFF)
    $bytes += [byte](($value -shr 24) -band [byte]0xFF)
    return $bytes
}
function Get-BGPHeader {
    param(
        [byte[]] $bytes
    )
    $header = @{}
    $header.Marker = GetBytes $bytes $BGP_HEADER_MARKER_OFFSET 16
    $header.Length = GetInt16 $bytes $BGP_HEADER_LENGTH_OFFSET
    $header.Type = $BGP_TYPES[(GetInt8 $bytes $BGP_HEADER_TYPE_OFFSET)]
    return $header

}
function New-BGPOpen {

    [byte[]] $bytes = @()
    for ($i = 0; $i -lt 16; $i++) {
        $bytes += [byte] 0xFF
    }
    $bytes = AddInt16 $bytes 0
    $bytes = AddInt8 $bytes 1  #OPEN
    $bytes = AddInt8 $bytes 4
    $bytes = AddInt16 $bytes $LocalASN  #64628
    $bytes = AddInt16 $bytes 180
    $bytes = AddInt32 $bytes (([IPAddress] $localIPAddress).Address)

    #Uncomment if no optional params:
    #$bytes = AddInt8 $bytes 0

    #opt parms - hardcoded for now to include:

    $bytes = AddInt8 $bytes 12 #opt params len
    $bytes = AddInt8 $bytes 2  #type: capability code
    $bytes = AddInt8 $bytes 10 #len

    # 1 - Multiprotocol extensions for BGP-4: 0101
    $bytes = AddInt8 $bytes 1  #capability code
    $bytes = AddInt8 $bytes 4  #len
    $bytes = AddInt8 $bytes 0
    $bytes = AddInt8 $bytes 1
    $bytes = AddInt8 $bytes 0
    $bytes = AddInt8 $bytes 1

    # 2 - Route Refresh Capability for BGP-4
    $bytes = AddInt8 $bytes 2  #capability code
    $bytes = AddInt8 $bytes 0  #len

    # 128 - Prestandard Route Refresh (deprecated)
    $bytes = AddInt8 $bytes 128  #capability code
    $bytes = AddInt8 $bytes 0    #len

    $bytes = SetInt16 $bytes $BGP_HEADER_LENGTH_OFFSET (29 + (GetInt8 $bytes $BGP_OPEN_OPTPARMLEN_OFFSET))
    return $bytes
}
function New-BGPKeepalive {

    [byte[]] $bytes = @()
    for ($i = 0; $i -lt 16; $i++) {
        $bytes += [byte] 0xFF
    }
    $bytes = AddInt16 $bytes 19
    $bytes = AddInt8 $bytes 4  #KEEPALIVE

    return $bytes
}


function Get-BGPUpdate {
    param(
        [byte[]] $bytes
    )
    $update = @{}

    $TotalLen = GetInt16 $bytes $BGP_HEADER_LENGTH_OFFSET

    $WithdrawnRoutesLen = GetInt16 $bytes $BGP_UPDATE_WITHDRAWN_OFFSET
    $PathAttributesLen = GetInt16 $bytes ($BGP_UPDATE_WITHDRAWN_OFFSET + $withdrawnRoutesLen + 2)
    $NetworkLayerLen = $TotalLen - 23 - $PathAttributesLen - $WithdrawnRoutesLen

    $WithdrawnRoutesStart = $BGP_UPDATE_WITHDRAWN_OFFSET
    $PathAttributesStart = $WithdrawnRoutesStart + 2 + $WithdrawnRoutesLen
    $NetworkLayerStart = $PathAttributesStart + 2 + $PathAttributesLen

    Write-Verbose "Total length: $TotalLen"
    Write-Verbose "Withdrawn routes start: $WithdrawnRoutesStart"
    Write-Verbose "Withdrawn routes len: $WithdrawnRoutesLen"
    Write-Verbose "Path Attributes start: $PathAttributesStart"
    Write-Verbose "Path Attributes len: $PathAttributesLen"
    Write-Verbose "Network Layer start: $NetworkLayerStart"
    Write-Verbose "Network Layer len: $NetworkLayerLen"

    Write-Verbose "Parsing Withdrawn Routes"
    $update.WithdrawnRoutes = @()
    $index = $WithdrawnRoutesStart + 2
    while ($index -lt $PathAttributesStart) {
        $PrefixBits = GetInt8 $bytes $index
        $PrefixBytes = [math]::ceiling($PrefixBits / 8)

        if ($PrefixBytes -gt 0) {
            $subnetBytes = GetBytes $bytes ($index + 1) $PrefixBytes
            for ($i = $PrefixBytes; $i -lt 4; $i++) {
                $subnetBytes += 0
            }
            $subnet = ([ipaddress] [byte[]]$subnetBytes).ipaddresstostring
            $update.WithdrawnRoutes += "$subnet/$prefixBits"
        }
        else {
            $update.WithdrawnRoutes += "0.0.0.0/0"
        }

        $index += $PrefixBytes + 1
    }

    Write-Verbose "Parsing Path Attributes"
    $update.PathAttributes = @()
    $index = $PathAttributesStart + 2
    while ($index -lt $NetworkLayerStart) {
        $PA = @{}
        $AttrFlags = GetInt8 $bytes ($index)
        $PA.Optional = [bool]($AttrFlags -band 128)
        $PA.Transitive = [bool]($AttrFlags -band 64)
        $PA.Partial = [bool]($AttrFlags -band 32)
        $PA.ExtendedLength = [bool]($AttrFlags -band 16)

        $PA.AttrType = GetBGPPathAttributeType(GetInt8 $bytes ($index + 1))

        if ($PA.ExtendedLength) {
            $AttrLen = GetInt16 $bytes ($index + 2)
            $AttrLenLen = 2
        }
        else {
            $AttrLen = GetInt8 $bytes ($index + 2)
            $AttrLenLen = 1
        }

        switch ($PA.AttrType) {
            "ORIGIN" {
                $PA.Value = $BGP_PATH_ATTRIBUTE_ORIGIN_VALUE[(GetInt8 $bytes ($index + 2 + $AttrLenLen))]
            }
            "AS_PATH" {
                $PA.ASPath = @()
                $pathindex = 0
                while ($pathindex -lt $AttrLen) {
                    $AttrValue = @{}
                    $AttrValue.PathSegmentType = $BGP_PATH_ATTRIBUTE_AS_PATH_SEGMENT_TYPE[(GetInt8 $bytes ($index + $pathindex + 2 + $AttrLenLen))]
                    $ASPaths = GetInt8 $bytes ($index + $pathindex + 4)
                    $ASIndex = 0
                    $AttrValue.ASes = @()
                    while ($ASIndex -lt $ASPaths) {
                        $AttrValue.ASes += GetInt16 $bytes ($index + $pathindex + 4 + $AttrLenLen + $ASIndex * 2)
                        $ASIndex += 1
                    }
                    $PA.ASPath += $AttrValue
                    $PathIndex += 2 + $ASPaths * 2
                }
                #<path segment type (1oct), path segment length (1oct), path segment value>
                #types: 1 AS_SET, 2 AS_SEQUENCE
                #value: set of ASes (Int16 ea)
            }
            "NEXT_HOP" {
                $PA.NextHop = ([ipaddress] (GetInt32 $bytes ($index + 2 + $AttrLenLen))).ipaddresstostring
            }
            { $_ -in "MULTI_EXIT_DISC", "LOCAL_PREF" } {
                $PA.Value = (GetInt32 $bytes ($index + 2 + $AttrLenLen))
            }
            "ATOMIC_AGGREGATE" {
                #Intentionally blank, no Attr Value
            }
            "AGGREGATOR" {
                $PA.AS = (GetInt16 $bytes ($index + 2 + $AttrLenLen))
                $PA.IPAddress = ([ipaddress] (GetInt32 $bytes ($index + 4 + $AttrLenLen))).ipaddresstostring
            }
            default {
                $PA.AttrValue = GetBytes $Bytes ($index + 2 + $AttrLenLen) $AttrLen
            }
        }

        $update.PathAttributes += $PA
        $index += $AttrLen + 2 + $AttrLenLen
    }

    Write-Verbose "Parsing Network Layer Reachability"

    $update.Prefixes = @()
    $index = $NetworkLayerStart

    while ($index -lt $TotalLen) {
        $PrefixBits = GetInt8 $bytes $index
        $PrefixBytes = [math]::ceiling($PrefixBits / 8)

        if ($PrefixBytes -gt 0) {
            $subnetBytes = GetBytes $bytes ($index + 1) $PrefixBytes
            for ($i = $PrefixBytes; $i -lt 4; $i++) {
                $subnetBytes += 0
            }
            $subnet = ([ipaddress] [byte[]]$subnetBytes).ipaddresstostring
            $update.Prefixes += "$subnet/$prefixBits"
        }
        else {
            $update.Prefixes += "0.0.0.0/0"
        }
        $Index += $PrefixBytes + 1
    }

    return $update
}

function Get-BGPOpen {
    param(
        [byte[]] $bytes
    )
    $open = @{}
    $open.Version = GetInt8 $bytes $BGP_OPEN_VERSION_OFFSET
    $open.AS = GetInt16 $bytes $BGP_OPEN_AS_OFFSET
    $open.HoldTime = GetInt16 $bytes $BGP_OPEN_HOLDTIME_OFFSET
    $open.BGPID = ([ipaddress] (GetInt32 $bytes $BGP_OPEN_BGPID_OFFSET)).ipaddresstostring
    $OptParmLen = GetInt8 $bytes $BGP_OPEN_OPTPARMLEN_OFFSET
    if ($optParmLen -gt 0) {
        $OptParms = GetBytes $bytes $BGP_OPEN_OPTPARM_OFFSET $OptParmLen
        $open.OptParams = @()
        $index = 0

        while ($index -lt $OptParmLen) {
            $newparam = @{}
            $newparamType = GetInt8 $OptParms ($index)
            $newparam.Type = $BGP_OPEN_OPTPARAM_TYPES[$newparamType]
            $newparamLength = GetInt8 $OptParms ($index + 1)
            $ParmValue = GetBytes $OptParms ($index + 2) ($newparamLength)
            $newparam.values = @()
            if ($newparamType -eq 2) {
                $capindex = 0
                while ($capindex -lt $newparamlength) {
                    $newcap = @{}
                    $newcap.Code = CapabilityCodeLookup (GetInt8 $ParmValue ($capindex))
                    $newcapLength = GetInt8 $ParmValue ($capindex + 1)
                    if ($newcaplength -gt 0) {
                        $newcap.value = GetBytes $ParmValue ($capindex + 2) ($newcaplength)
                    }
                    $newparam.values += $newcap
                    $capindex += $newcapLength + 2
                }
            }
            $open.OptParams += $newparam
            $index += $newparamLength + 2
        }
    }
    return $open
}

function Get-BGPNotification {
    param(
        [byte[]] $bytes
    )
    $notification = @{}
    $notification.ErrorCode = $BGP_ERROR_CODES[(GetInt8 $bytes $BGP_NOTIFICATION_CODE_OFFSET)]
    if ((GetInt8 $bytes $BGP_NOTIFICATION_CODE_OFFSET) -eq 1) {
        #Message
        $notification.ErrorSubcode = $BGP_ERROR_SUBCODE_MESSAGE[(GetInt8 $bytes $BGP_NOTIFICATION_SUBCODE_OFFSET)]
    }
    elseif ((GetInt8 $bytes $BGP_NOTIFICATION_CODE_OFFSET) -eq 2) {
        #OPEN
        $notification.ErrorSubcode = $BGP_ERROR_SUBCODE_OPEN[(GetInt8 $bytes $BGP_NOTIFICATION_SUBCODE_OFFSET)]
    }
    elseif ((GetInt8 $bytes $BGP_NOTIFICATION_CODE_OFFSET) -eq 6) {
        #CEASE
        $notification.ErrorSubcode = $BGP_ERROR_SUBCODE_CEASE[(GetInt8 $bytes $BGP_NOTIFICATION_SUBCODE_OFFSET)]
    }
    else {
        $notification.ErrorSubcode = GetInt8 $bytes $BGP_NOTIFICATION_SUBCODE_OFFSET
    }
    $notification.Data = GetInt16 $bytes $BGP_NOTIFICATION_DATA_OFFSET ((GetInt16 $bytes $BGP_HEADER_LENGTH_OFFSET) - 21)


    return $notification
}


function Set-BGPState {
    param(
        [BGPState] $State
    )

    Write-Verbose "BGP state change from $($Script:BGPState) to $State"
    $Script:bgpState = $State
}

function Get-BGPState {
    return $Script:bgpState
}


enum BGPOptParamType {
    Authentication
    Capabilities
}

enum BGPState {
    Idle
    Connect
    Active
    OpenSent
    OpenConfirm
    Established
    Custom
}

enum BGPOrigin {
    EGP
    IGP
    Incomplete
}

class BGPCapability {
    [String] $Code
    [byte[]] $Value
    [string]ToString() {
        return ($this.code)
    }
}
class BGPOptParam {
    [BGPOptParamType] $Type
    [BGPCapability[]] $Capabilities
    [string]ToString() {
        return ($this.type)
    }
}

class BGPPath {
    [string] $prefix
    [string] $NextHop
    [Int32[]] $Path
    [String] $LocPrf
    [Int32] $Metric
    [BGPOrigin] $Origin
    [string]ToString() {
        return ($this.prefix)
    }
}

class BGPPeer {
    [string] $LocalIPAddress
    [int32] $LocalAS
    [string] $RouterIPAddress
    [int32] $RouterAS
    [int16] $HoldTime
    [int16] $Version
    [string] $BGPID
    [BGPState] $State
    [BGPOptParam[]] $OptParams
    [BGPPath[]] $Routes
}

class BGPHost {
    [string] $ComputerName
    [string] $LocalIPAddress
}

function New-SdnExpressBGPHost {
    [CmdletBinding()]
    param(
        [String] $ComputerName = "localhost",
        [string] $SwitchName = "",
        [String] $LocalIPAddress,
        [Int32] $PrefixLength,
        [Int32] $VLANID = 0
    )

    #TODO: remember gateway parameter and during test add /32 route only if needed
    #TODO: test for hyper-v and Hyper-v powershell PS

    if ([String]::IsNullorEmpty($ComputerName) ) {
        Write-Verbose "Running locally."
        $Session = @{}
    }
    else {
        Write-Verbose "Running on $ComputerName."
        $Session = @{
            session = new-pssession -computername $ComputerName
        }
    }

    Invoke-Command @session -ArgumentList $SwitchName, $LocalIPAddress, $PrefixLength, $VLANID {
        param(
            [string] $SwitchName,
            [String] $LocalIPAddress,
            [Int32] $PrefixLength,
            [Int32] $VLANID
        )
        if ([string]::IsNullOrEmpty($SwitchName)) {
            $vmswitch = Get-vmswitch
            if ($null -ieq $vmswitch) {
                throw "No virtual switch found."
            }
            if ($vmswitch.count -gt 1) {
                throw "Hyper-V host contains more than one virtual switch. Use SwitchName parameter to select a virtual switch."
            }
            $SwitchName = $vmswitch.name
        }

        Get-vmnetworkadapter -managementos -Name BGP -erroraction silentlycontinue | remove-vmnetworkadapter

        Add-vmnetworkadapter -ManagementOS -SwitchName $SwitchName -Name "BGP" | out-null
        Set-VMNetworkAdapterVlan -ManagementOS -VMNetworkAdaptername "BGP" -VlanId $VLANID -Access | out-null
        Set-NetIPInterface -InterfaceAlias "vEthernet (BGP)" -Dhcp Disabled | out-null
        Set-dnsclient -InterfaceAlias "vEthernet (BGP)" -RegisterThisConnectionsAddress $False | out-null

        new-NetIPAddress -IPAddress $LocalIPAddress -InterfaceAlias "vEthernet (BGP)" -PrefixLength $PrefixLength  | out-null

    }

    $BGPHost = [BGPHost]::New()
    $BGPhost.Computername = $ComputerName
    $BGPhost.LocalIPAddress = $LocalIPAddress
    return $BGPHost
}

function Remove-SdnExpressBGPHost {
    [CmdletBinding()]
    param(
        [string] $ComputerName = $null,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [BGPHost] $BGPHost = $null
    )

    if ($BGPHost) {
        $computername = $BGPHost.ComputerName
    }

    if ([String]::IsNullorEmpty($Computername) ) {
        Write-Verbose "Running locally."
        $Session = @{}
    }
    else {
        Write-Verbose "Running on $ComputerName."
        $Session = @{
            session = new-pssession -computername $ComputerName
        }
    }

    Invoke-Command @session {
        Get-vmnetworkadapter -managementos -Name BGP -erroraction silentlycontinue | remove-vmnetworkadapter
    }
}


function Test-SdnExpressBGP {
    [CmdletBinding()]
    param(
        [String] $RouterIPAddress,
        [String] $LocalIPAddress,
        [String] $LocalASN,
        [int32] $Wait = 3,
        [String] $ComputerName = "localhost",
        [Switch] $force,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [BGPHost] $BGPHost = $null
    )

    if ($BGPHost) {
        $ComputerName = $BGPHost.ComputerName
        $LocalIPAddress = $BGPHost.LocalIPAddress
    }

    if ([String]::IsNullorEmpty($Computername) ) {
        Write-Verbose "Running locally."
        $Session = @{}
    }
    else {
        Write-Verbose "Running on $ComputerName."
        $Session = @{
            session = new-pssession -computername $ComputerName
        }
    }

    $BGP_HEADER_LEN = 19
    $BGP_HEADER_MARKER_OFFSET = 0
    $BGP_HEADER_LENGTH_OFFSET = 16
    $BGP_HEADER_TYPE_OFFSET = 18
    $BGP_TYPES = @("", "OPEN", "UPDATE", "NOTIFICATION", "KEEPALIVE", "ROUTEREFRESH")

    $BGP_OPEN_VERSION_OFFSET = $BGP_HEADER_LEN + 0
    $BGP_OPEN_AS_OFFSET = $BGP_HEADER_LEN + 1
    $BGP_OPEN_HOLDTIME_OFFSET = $BGP_HEADER_LEN + 3
    $BGP_OPEN_BGPID_OFFSET = $BGP_HEADER_LEN + 5
    $BGP_OPEN_OPTPARMLEN_OFFSET = $BGP_HEADER_LEN + 9
    $BGP_OPEN_OPTPARM_OFFSET = $BGP_HEADER_LEN + 10
    $BGP_OPEN_OPTPARAM_TYPES = @("", "Authentication (deprecated)", "Capabilities")

    $BGP_ERROR_CODES = @("", "Message Header Error", "OPEN Message Error", "UPDATE Message Error", "Hold Timer Expired", "Finite State Machine Error", "Cease")
    $BGP_ERROR_SUBCODE_MESSAGE = @("", "Connection Not Synchronized.", "Bad Message Length.", "Bad Message Type.")
    $BGP_ERROR_SUBCODE_OPEN = @("", "Unsupported Version Number.", "Bad Peer AS.", "Bad BGP Identifier.", "Unsupported Optional Parameter.", "5 [Deprecated]", "Unacceptable Hold Time.")
    $BGP_ERROR_SUBCODE_CEASE = @("", "Maximum Number of Prefixes Reached.", "Administrative Shutdown.", "Peer De-configured.", "Administrative Reset.", "Connection Rejected.", "Other Configuration Change.", "Connection Collision Resolution.", "Out of Resources.")

    $BGP_NOTIFICATION_CODE_OFFSET = $BGP_HEADER_LEN + 0
    $BGP_NOTIFICATION_SUBCODE_OFFSET = $BGP_HEADER_LEN + 1
    $BGP_NOTIFICATION_DATA_OFFSET = $BGP_HEADER_LEN + 2

    $BGP_UPDATE_WITHDRAWN_OFFSET = $BGP_HEADER_LEN
    $BGP_PATH_ATTRIBUTE_TYPES = @("", "ORIGIN", "AS_PATH", "NEXT_HOP", "MULTI_EXIT_DISC", "LOCAL_PREF", "ATOMIC_AGGREGATE", "AGGREGATOR")
    $BGP_PATH_ATTRIBUTE_ORIGIN_VALUE = @("IGP", "EGP", "INCOMPLETE")
    $BGP_PATH_ATTRIBUTE_AS_PATH_SEGMENT_TYPE = @("", "AS_SET", "AS_SEQUENCE")

    [BGPState] $Script:bgpState = [BGPState]::Idle
    Set-BGPState Idle
    $Results = [BGPPeer]::new()
    $results.LocalIPAddress = $LocalIPAddress
    $results.LocalAS = $LocalASN
    $results.RouterIPAddress = $RouterIPAddress

    try {
        Write-Verbose "Attempting BGP connection from $localIPAddress to $RouterIPAddress"
        Invoke-Command @Session -argumentlist $LocalIPAddress,$RouterIPAddress,$wait,$force {
            param(
                $LocalIPAddress,
                $RouterIPAddress,
                $wait,
                $force
            )

            $port = "179"

            $RestoreMuxState = $false
            $mux = Get-Service -Name 'SlbMux' -ErrorAction SilentlyContinue
            if ($null -ne $mux) {
                $muxstartup = $mux.starttype
                $muxstatus = $mux.status

                if (($muxstatus -ne "Stopped") -or ($Muxstartup -ne "Disabled")) {
                    if ($force) {
                        $RestoreMuxState = $true
                        Set-Service -Name -startup Disabled
                        stop-Service -Name
                    }
                    else {
                        throw "SLB Mux service is active. Use -force to temporarily disable it during test."
                    }
                }
            }
            else {
                $muxstate = $null
            }

            $IPEndpoint = New-object System.Net.IPEndPoint([IPAddress]$LocalIPAddress, 0)
            try {
                $tcp = New-Object System.Net.Sockets.TcpClient($IPEndpoint)
            }
            catch {
                throw "Local IP address $LocalIPAddress not found on computer $(hostname)."
            }

            try {
                $tcp.Connect($routerIPAddress, $Port)
            }
            catch {
                throw "BGP Listener not found at RouterIPAddress $RouterIPAddress."
            }

            $tcpstream = $tcp.GetStream()
            $reader = New-Object System.IO.BinaryReader($tcpStream)
            $writer = New-Object System.IO.BinaryWriter($tcpStream)

            $reader.BaseStream.ReadTimeout = $Wait * 1000
        }

        Set-BGPState -State Connect
        $IsConnected = Invoke-Command @Session { $tcp.connected }
        if ($IsConnected) {
            Write-Verbose "BGP Connection Initiated."

            #Send OPEN
            $chars = new-BGPOpen

            Write-Verbose "Sending BGP OPEN"
            Write-Verbose "Write bytes[$($chars.count)] $chars"

            Invoke-Command @Session -argumentlist (, $chars) {
                param(
                    [byte[]] $chars
                )
                $writer.Write([byte[]]$chars)
                $writer.Flush()
            }

            Write-Verbose "Write complete."
            Set-BGPState OpenSent

            Write-Verbose "Entering read loop."
            do {
                try {
                    $chars = Invoke-Command @Session {
                        try {
                            $chars = @()
                            $chars = @($reader.Readbyte())
                            while (($reader.PeekChar() -ne -1) -or ($tcp.Available)) {
                                $chars += $reader.Readbyte()
                            }
                            return $chars
                        }
                        catch {
                            #return @()
                            if ($_.Exception.InnerException.InnerException.NativeErrorCode -eq 10060) {
                                #timedout
                                throw "Timeout"
                            }
                            else {
                                throw $_
                            }
                        }
                    }
                }
                catch {
                    Write-Verbose "Caught. $($_)"
                    if ($_.exception.Message -eq "Timeout") {
                        #timedout NativeErrorCode 10060
                        if (!$bytesRemain) {
                            Write-Verbose "Timeout, no updates recieved within $Wait seconds. Exiting."
                            break
                        }
                    }
                    else {
                        $err = "Connection closed. BGP active at routerIPAddress, but session rejected by remote based on localIPAddress."
                        Write-Verbose $err
                        Set-BGPState Idle
                        throw $err
                    }
                }
                $bytesRemain = $chars.count

                while ($bytesremain -gt 0) {
                    Write-Verbose "Received data, parsing header. Buffer contains $bytesRemain bytes."
                    Write-Verbose "Buffer bytes[$($chars.count)] $chars"

                    $header = Get-BGPHeader $chars
                    Write-Verbose ($header | ConvertTo-Json -Depth 10)
                    $bytesRemain -= $header.Length
                    Write-Verbose "$bytesRemain bytes remain to parse."

                    switch ($header.Type) {
                        "OPEN" {
                            Write-Verbose "Parsing OPEN message."
                            $open = Get-BGPOpen $chars
                            Write-Verbose ($open | ConvertTo-Json -Depth 10)

                            $Results.RouterAS = $open.AS
                            $Results.HoldTime = $open.HoldTime
                            $Results.Version = $open.Version
                            $Results.BGPID = $open.BGPID
                            foreach ($optparam in $open.optparams) {
                                $op = [BGPOptParam]::New()
                                $op.Type = $optparam.type
                                foreach ($cap in $optparam.values) {
                                    $c = [BGPCapability]::new()
                                    $c.Code = $cap.Code
                                    $c.Value = $cap.value
                                    $op.Capabilities += $c
                                }
                                $results.OptParams += $op
                            }
                            Set-BGPState OpenConfirm
                        }
                        "KEEPALIVE" {
                            if ((Get-BGPState) -in [BGPState]::OpenConfirm, [BGPState]::Established) {
                                $chars = New-BGPKeepalive

                                Write-Verbose "Sending BGP Keepalive"
                                Write-Verbose "Write bytes[$($chars.count)] $chars"
                                Invoke-Command @Session -argumentlist (, $chars) {
                                    param(
                                        $chars
                                    )
                                    $writer.Write([byte[]]$chars)
                                    $writer.Flush()
                                }

                                Set-BGPState -State Established
                                Write-Verbose "Success, BGP session established!"
                            }
                            else {
                                Write-Verbose "Out of order Keepalive received in state $(Get-BGPState)."
                            }
                        }
                        "NOTIFICATION" {
                            Write-Verbose "Parsing NOTIFICATION message."
                            $open = Get-BGPNotification $chars
                            Write-Verbose ($open | ConvertTo-Json -Depth 10)
                            Write-Verbose "BGP peer found, but connection refused."
                            Set-BGPState -State Idle
                            throw "BGP peer found, but connection refused. ErrorCode: $($open.Errorcode), ErrorSubcode: $($open.ErrorSubcode)"
                        }
                        "UPDATE" {
                            Write-Verbose "Parsing UPDATE message."
                            $update = Get-bgpupdate $chars
                            Write-Verbose ($update | ConvertTo-Json -Depth 10)
                            $NextHop = ($Update.PathAttributes | where-object { $_.AttrType -eq "NEXT_HOP" }).NextHop
                            $ASPath = ($Update.PathAttributes | where-object { $_.AttrType -eq "AS_PATH" }).ASPath.ASes
                            $Origin = ($Update.PathAttributes | where-object { $_.AttrType -eq "ORIGIN" }).Value
                            $LocPrf = ($Update.PathAttributes | where-object { $_.AttrType -eq "LOCAL_PREF" }).Value
                            $Metric = ($Update.PathAttributes | where-object { $_.AttrType -eq "MULTI_EXIT_DISC" }).Value

                            foreach ($prefix in $Update.Prefixes) {
                                $BGPRoute = [BGPPath]::New()
                                $BGPRoute.Prefix = $Prefix
                                $BGPRoute.NextHop = $NextHop
                                $BGPRoute.Path = $ASPath
                                $BGPRoute.LocPrf = $LocPrf
                                $BGPRoute.Metric = $Metric
                                $BGPRoute.Origin = $Origin

                                $Results.Routes += $BGPRoute
                            }
                        }
                        "ROUTEREFRESH" {
                            Write-Verbose "Parsing ROUTEREFRESH message."
                            Set-BGPState Custom
                        }
                    }

                    $chars = getBytes $chars ($header.length) $bytesremain
                    Write-Verbose "BGP State: $(Get-BGPState)"
                    Write-Verbose "Returning to read loop, waiting up to $wait seconds for more data."
                }
            } until ((Get-BGPState) -in [BGPState]::Custom, [BGPState]::Idle)
        }
        else {
            Write-Verbose "Not connected."
            throw "Listener found at BGP port 179 of $RouterIPAddress, but it closed the connection from $LocalIPAddress."
        }
    }
    finally {
        Invoke-Command @Session {
            if ($null -ne $reader) {
                $reader.Close()
            }
            if ($null -ne $writer) {
                $writer.Close()
            }
            if ($null -ne $tcp) {
                $tcp.Close()
            }
            if ($RestoreMuxState) {
                Set-Service -Name SlbMux -StartupType $MuxStartup
                if ($MuxStatus -eq "Running") {
                    Start-Service -Name SlbMux
                }
            }
        }
        if (![String]::IsNullorEmpty($Computername) ) {
            remove-pssession $session.session
        }
    }
    $results.State = (Get-BGPState)
    $results
}

Export-ModuleMember -Function Test-SdnExpressBGP
Export-ModuleMember -Function New-SdnExpressBGPHost
Export-ModuleMember -Function Remove-SdnExpressBGPHost

# SIG # Begin signature block
# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDQL1hkvsgenu6Y
# Xbd4qs3/Ln7MdZq8/AxrYz4mk5tCV6CCDXYwggX0MIID3KADAgECAhMzAAAEBGx0
# Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz
# NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo
# DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3
# a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF
# HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy
# 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC
# Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj
# L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp
# h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3
# cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X
# dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL
# E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi
# u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1
# sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq
# 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb
# DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/
# V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIK7L9sARe1C3iyvnAf4/dS2S
# 68l/K0SVXtEykXzCwmIzMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAOPlsk5YH5C0XmqXuLu/3j80bl7YnYCRhuoe2wR+jtiSuBOxlovtBILzX
# tb4tpU1NM/trjleInitiKXE4ls9YUIYGpq5ZY69/z3So/L58UUoZIzXcN8dugA3C
# nyVLPyq6gnOTXXW25EdzqnVlK4OpwmXYbNJmeKkbCnpzD3Ya01zna70jG44I4Dmo
# q/tbV3DWbfPxdGfGNJMzOykpr5ghe0jCsJ2mNlP31c5jF559bcf7toAAkty4TDNl
# MqJEmfWBaaQeu00Hv0eFAzu18xI7yVpbD/Yq14c9F8o9JNKnS2upXLDheWmkbjgJ
# P5wstrsWpsg57iZ3tDEytghrfrSTM6GCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC
# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCBc3XOHX63VJmXUKGcQS4lvUnW+szIMcXg74N4FKrIcqgIGZxqQUZi9
# GBMyMDI0MTEwNjE5MjMzMy4zMTlaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046RTAwMi0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHqMIIHIDCCBQigAwIBAgITMwAAAe4F0wIwspqdpwABAAAB7jANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1
# NDRaFw0yNTAzMDUxODQ1NDRaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046RTAwMi0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQC+8byl16KEia8xKS4vVL7REOOR7LzYCLXEtWgeqyOV
# lrzuEz+AoCa4tBGESjbHTXECeMOwP9TPeKaKalfTU5XSGjpJhpGx59fxMJoTYWPz
# zD0O2RAlyBmOBBmiLDXRDQJL1RtuAjvCiLulVQeiPI8V7+HhTR391TbC1beSxwXf
# dKJqY1onjDawqDJAmtwsA/gmqXgHwF9fZWcwKSuXiZBTbU5fcm3bhhlRNw5d04Ld
# 15ZWzVl/VDp/iRerGo2Is/0Wwn/a3eGOdHrvfwIbfk6lVqwbNQE11Oedn2uvRjKW
# EwerXL70OuDZ8vLzxry0yEdvQ8ky+Vfq8mfEXS907Y7rN/HYX6cCsC2soyXG3OwC
# tLA7o0/+kKJZuOrD5HUrSz3kfqgDlmWy67z8ZZPjkiDC1dYW1jN77t5iSl5Wp1HK
# Bp7JU8RiRI+vY2i1cb5X2REkw3WrNW/jbofXEs9t4bgd+yU8sgKn9MtVnQ65s6QG
# 72M/yaUZG2HMI31tm9mooH29vPBO9jDMOIu0LwzUTkIWflgd/vEWfTNcPWEQj7fs
# WuSoVuJ3uBqwNmRSpmQDzSfMaIzuys0pvV1jFWqtqwwCcaY/WXsb/axkxB/zCTdH
# SBUJ8Tm3i4PM9skiunXY+cSqH58jWkpHbbLA3Ofss7e+JbMjKmTdcjmSkb5oN8qU
# 1wIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFBCIzT8a2dwgnr37xd+2v1/cdqYIMB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQB3ZyAva2EKOWSVpBnYkzX8f8GZjaOs577F
# 9o14Anh9lKy6tS34wXoPXEyQp1v1iI7rJzZVG7rpUznay2n9csfn3p6y7kYkHqtS
# ugCGmTiiBkwhFfSByKPI08MklgvJvKTZb673yGfpFwPjQwZeI6EPj/OAtpYkT7IU
# XqMki1CRMJKgeY4wURCccIujdWRkoVv4J3q/87KE0qPQmAR9fqMNxjI3ZClVxA4w
# iM3tNVlRbF9SgpOnjVo3P/I5p8Jd41hNSVCx/8j3qM7aLSKtDzOEUNs+ZtjhznmZ
# gUd7/AWHDhwBHdL57TI9h7niZkfOZOXncYsKxG4gryTshU6G6sAYpbqdME/+/g1u
# er7VGIHUtLq3W0Anm8lAfS9PqthskZt54JF28CHdsFq/7XVBtFlxL/KgcQylJNni
# a+anixUG60yUDt3FMGSJI34xG9NHsz3BpqSWueGtJhQ5ZN0K8ju0vNVgF+Dv05si
# rPg0ftSKf9FVECp93o8ogF48jh8CT/B32lz1D6Truk4Ezcw7E1OhtOMf7DHgPMWf
# 6WOdYnf+HaSJx7ZTXCJsW5oOkM0sLitxBpSpGcj2YjnNznCpsEPZat0h+6d7ulRa
# WR5RHAUyFFQ9jRa7KWaNGdELTs+nHSlYjYeQpK5QSXjigdKlLQPBlX+9zOoGAJho
# Zfrpjq4nQDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN
# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkUwMDItMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQCI
# o6bVNvflFxbUWCDQ3YYKy6O+k6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6tYxWDAiGA8yMDI0MTEwNjE4MTgw
# MFoYDzIwMjQxMTA3MTgxODAwWjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDq1jFY
# AgEAMAcCAQACAh9YMAcCAQACAhMiMAoCBQDq14LYAgEAMDYGCisGAQQBhFkKBAIx
# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI
# hvcNAQELBQADggEBADLSnc4WaS0gvgWnvth2EzEWThjG2fV+RMObE70Lk/n40GIN
# 8mb01MMi3RRV0/Ou2lK4E/qqgt/tJO6l+jUxaXAMhpadkZxQLtnB5JctEqkFpALR
# e0CY9U5U0jr2+OWdRVMxhGJ1rnpSgY4b+Va1HrM6wGXWUtUGFh0dJuGJOQwLzARf
# 3uxhABxQ3wWwiQ4vAp2ollMCqM9OTd3qZWHe0L3dqkOc2VKB4bFjLVKvHC4RfH8k
# ml0lmjeWE4F8T6ZQkz5g0vmKMci1ZuQurCd4BrvK4YT22z8Fb0whSwSBsonaWdYM
# MvnGP5naRbLWET8vE9h9LlATxRyp7XZC9N5E17QxggQNMIIECQIBATCBkzB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAe4F0wIwspqdpwABAAAB7jAN
# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G
# CSqGSIb3DQEJBDEiBCAu0YjfF+jVRvKCrSvUcilZjlhwdq6UPr+gFMq0teGcIDCB
# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIE9QdxSVhfq+Vdf+DPs+5EIkBz9o
# CS/OQflHkVRhfjAhMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAHuBdMCMLKanacAAQAAAe4wIgQgGHClkOFC0tJWwqp9Ypa2/BzAa9Wi
# FyyEcPFAkAsFvaIwDQYJKoZIhvcNAQELBQAEggIAsu5ObM/YYCSMSHjHBlA21tWZ
# wlx/VYcfSv/kCZiLjYrQIZ755WrPxSXyk8G5m33IRJ9qE2W85/tIgP/nagmyVnnL
# DA6kDxG1EM0dOOdsER0TU2Lti2XfMpijhQZN5WOgmkTejVNY4U+DE5H3Z6y/06mv
# nPOACgiLgkXId1tDeRdCiKZ70jen8KEbAqWO6sjhCbW3kVIiIi3tuoiL7FoYaz0K
# hX2l2smAQgYHxQ297+sMVj5/i6WHqDG79EI2QdeKq9MAAdG2YZ2od97Gdn+QN8IR
# XtNVlguidhVkBX6/wrxbYyWdhVpeNRT14DIvQkfWQKWG7rRfuWZmJuNBAH28PKeg
# Yav/I59jd94Mpvys7q77J0UDwwKRRPMfZn5u1ST8huBEtQctN2yuzU7OzJx/KSZZ
# Cu1Oj75MezSe8CIFY3FTxBHtslxmUeRkR8FE+rmfL6DnR/0IGmP+s1U8QaxdZ8eW
# MUNcJA9DQpUJ+8zQmCnGYhq+JFMcvptYig4RcNjI/4dsrzxXo3nNFonJj9FtsQTi
# goYV8bg3ZTCC4Ir0vFrPwfyKrxOIECpb5SwCWSqpoqF5daFCKxS3xY52suzWDnv2
# hTO/xEIsnu24JKpc1MWwauBUqkI0kOyREurRpBciS0x9mN5qUyKAPJY4VBqTuTlc
# 7aFvuEs3pbly5orVN2U=
# SIG # End signature block