Public/New-BgpUpdate.ps1

function New-BgpUpdate {
    <#
    .SYNOPSIS
        Builds a BGP UPDATE (4-octet AS_PATH) advertising and/or withdrawing IPv4 and IPv6 prefixes.
        -EndOfRib builds the End-of-RIB marker for -AddressFamily (RFC 4724).
    .DESCRIPTION
        IPv4 prefixes use the classic NLRI/Withdrawn fields. IPv6 prefixes are carried in MP_REACH_NLRI / MP_UNREACH_NLRI
        (RFC 4760). One UPDATE advertises a single family: with IPv6 -Nlri, -NextHop must be an IPv6 address.
    .EXAMPLE
        New-BgpUpdate -Nlri 10.10.0.0/24,10.10.1.0/24 -NextHop 192.168.1.99 -AsPath 65001
    .EXAMPLE
        New-BgpUpdate -Nlri 2001:db8:10::/48 -NextHop 2001:db8:ffff::99 -AsPath 65001
    #>

    [CmdletBinding()]
    param(
        [string[]]$Nlri = @(),
        [string[]]$Withdrawn = @(),
        [ipaddress]$NextHop,
        [uint32[]]$AsPath = @(),
        [ValidateSet('IGP', 'EGP', 'Incomplete')][string]$Origin = 'IGP',
        [uint32]$LocalPref,
        [uint32]$Med,
        [string[]]$Community = @(),
        [switch]$EndOfRib,
        [ValidateSet('IPv4', 'IPv6')][string]$AddressFamily = 'IPv4'
    )

    function ConvertTo-PrefixBytes([string]$Cidr) {
        $ip, $len = $Cidr -split '/'
        $len = [int]$len
        $a = [ipaddress]$ip
        $max = if ($a.AddressFamily -eq 'InterNetworkV6') { 128 } else { 32 }
        if ($len -lt 0 -or $len -gt $max) { throw "Invalid prefix length in '$Cidr'." }
        $addr = $a.GetAddressBytes()
        $n = [int][Math]::Ceiling($len / 8)
        $out = [byte[]]::new(1 + $n)
        $out[0] = [byte]$len
        for ($i = 0; $i -lt $n; $i++) { $out[1 + $i] = $addr[$i] }
        if ($n -gt 0 -and ($len % 8)) { $out[$n] = [byte]($out[$n] -band (0xFF -shl (8 - ($len % 8)))) }
        , $out
    }

    function New-PathAttribute([byte]$Flags, [byte]$Code, [byte[]]$Value) {
        if ($Value.Length -gt 255) {
            New-Packet -Field @(
                New-PacketField Flags ($Flags -bor 0x10) -Bytes 1
                New-PacketField Code  $Code -Bytes 1
                New-PacketField Len   $Value.Length -Bytes 2
                New-PacketField Value $Value
            )
        }
        else {
            New-Packet -Field @(
                New-PacketField Flags $Flags -Bytes 1
                New-PacketField Code  $Code -Bytes 1
                New-PacketField Len   $Value.Length -Bytes 1
                New-PacketField Value $Value
            )
        }
    }

    $isV6 = { param($p) $p -match ':' }
    $nlri4 = @($Nlri | Where-Object { -not (& $isV6 $_) })
    $nlri6 = @($Nlri | Where-Object { & $isV6 $_ })
    $wd4 = @($Withdrawn | Where-Object { -not (& $isV6 $_) })
    $wd6 = @($Withdrawn | Where-Object { & $isV6 $_ })
    if ($nlri4.Count -and $nlri6.Count) { throw 'Advertise IPv4 and IPv6 prefixes in separate UPDATEs.' }

    $attrs = [System.Collections.Generic.List[byte]]::new()

    if ($Nlri.Count) {
        $originCode = @{ IGP = 0; EGP = 1; Incomplete = 2 }[$Origin]
        $attrs.AddRange([byte[]](New-PathAttribute 0x40 1 ([byte[]]@($originCode))))

        $segment = [byte[]]@()
        if ($AsPath.Count) {
            $fields = @(
                New-PacketField SegType 2 -Bytes 1   # AS_SEQUENCE
                New-PacketField SegLen  $AsPath.Count -Bytes 1
            )
            $fields += foreach ($a in $AsPath) { New-PacketField Asn $a -Bytes 4 }
            $segment = New-Packet -Field $fields
        }
        $attrs.AddRange([byte[]](New-PathAttribute 0x40 2 $segment))

        if (-not $NextHop) { throw '-NextHop is required when advertising -Nlri.' }
        if ($nlri4.Count) {
            if ($NextHop.AddressFamily -ne 'InterNetwork') { throw 'An IPv4 -Nlri needs an IPv4 -NextHop.' }
            $attrs.AddRange([byte[]](New-PathAttribute 0x40 3 $NextHop.GetAddressBytes()))
        }

        if ($PSBoundParameters.ContainsKey('Med')) {
            $attrs.AddRange([byte[]](New-PathAttribute 0x80 4 (New-Packet -Field (New-PacketField V $Med -Bytes 4))))
        }
        if ($PSBoundParameters.ContainsKey('LocalPref')) {
            $attrs.AddRange([byte[]](New-PathAttribute 0x40 5 (New-Packet -Field (New-PacketField V $LocalPref -Bytes 4))))
        }
        if ($Community.Count) {
            $cf = foreach ($c in $Community) {
                $hi, $lo = $c -split ':'
                New-PacketField C ([uint32](([uint32]$hi -shl 16) -bor [uint32]$lo)) -Bytes 4
            }
            $attrs.AddRange([byte[]](New-PathAttribute 0xC0 8 (New-Packet -Field $cf)))
        }
        if ($nlri6.Count) {
            if ($NextHop.AddressFamily -ne 'InterNetworkV6') { throw 'An IPv6 -Nlri needs an IPv6 -NextHop.' }
            $mp = [System.Collections.Generic.List[byte]]::new()
            $mp.AddRange([byte[]](New-Packet -Field @(
                        New-PacketField Afi 2 -Bytes 2
                        New-PacketField Safi 1 -Bytes 1
                        New-PacketField NhLen 16 -Bytes 1
                        New-PacketField Nh $NextHop.GetAddressBytes()
                        New-PacketField Reserved 0 -Bytes 1)))
            foreach ($p in $nlri6) { $mp.AddRange([byte[]](ConvertTo-PrefixBytes $p)) }
            $attrs.AddRange([byte[]](New-PathAttribute 0x80 14 $mp.ToArray()))
        }
    }
    if ($EndOfRib -and $AddressFamily -eq 'IPv6') {
        # RFC 4724 End-of-RIB for IPv6: an MP_UNREACH_NLRI with no prefixes (IPv4 is just an empty UPDATE)
        $attrs.AddRange([byte[]](New-PathAttribute 0x80 15 ([byte[]](New-Packet -Field @(
                            New-PacketField Afi 2 -Bytes 2
                            New-PacketField Safi 1 -Bytes 1)))))
    }
    if ($wd6.Count) {
        $mp = [System.Collections.Generic.List[byte]]::new()
        $mp.AddRange([byte[]](New-Packet -Field @(
                    New-PacketField Afi 2 -Bytes 2
                    New-PacketField Safi 1 -Bytes 1)))
        foreach ($p in $wd6) { $mp.AddRange([byte[]](ConvertTo-PrefixBytes $p)) }
        $attrs.AddRange([byte[]](New-PathAttribute 0x80 15 $mp.ToArray()))
    }

    $wd = [System.Collections.Generic.List[byte]]::new()
    foreach ($p in $wd4) { $wd.AddRange([byte[]](ConvertTo-PrefixBytes $p)) }
    $nl = [System.Collections.Generic.List[byte]]::new()
    foreach ($p in $nlri4) { $nl.AddRange([byte[]](ConvertTo-PrefixBytes $p)) }

    $body = New-Packet -Field @(
        New-PacketField WithdrawnLen $wd.Count -Bytes 2
        New-PacketField Withdrawn    $wd.ToArray()
        New-PacketField AttrLen      $attrs.Count -Bytes 2
        New-PacketField Attributes   $attrs.ToArray()
        New-PacketField Nlri         $nl.ToArray()
    )
    New-BgpMessage -Type 2 -Body $body
}