Skip to content

IPv6 Unicast

Thomas Mangin edited this page Mar 6, 2026 · 1 revision

IPv6 Unicast

IPv6 Unicast is the BGP address family for advertising IPv6 prefixes for unicast (one-to-one) traffic forwarding. As IPv6 adoption continues globally, ExaBGP provides full support for IPv6 unicast route announcements, withdrawals, and reception, enabling applications to programmatically control IPv6 routing using the same API patterns as IPv4.

Table of Contents


Overview

IPv6 Unicast is the BGP address family (AFI 2, SAFI 1) for distributing IPv6 routing information via MP-BGP (Multiprotocol BGP per RFC 4760).

Key Characteristics:

  • Address Family Identifier (AFI): 2 (IPv6)
  • Subsequent Address Family Identifier (SAFI): 1 (Unicast)
  • Prefix Format: IPv6 CIDR notation (e.g., 2001:db8::/32)
  • Next-Hop: IPv6 address (or IPv4-mapped IPv6 in some cases)

What IPv6 Unicast Routes Carry:

  • IPv6 prefix (network + prefix length)
  • Next-hop IPv6 address
  • BGP path attributes (AS-PATH, MED, LOCAL_PREF, COMMUNITY, etc.)

MP-BGP: Unlike IPv4 unicast (which uses traditional BGP UPDATE format), IPv6 uses MP-BGP extensions with MP_REACH_NLRI and MP_UNREACH_NLRI attributes.


When to Use IPv6 Unicast

Use IPv6 unicast for:

IPv6 Internet Routing: Global IPv6 routing, ISP peering ✅ Dual-Stack Networks: Running IPv4 and IPv6 in parallel ✅ IPv6-Only Networks: Modern cloud and mobile networks ✅ Anycast IPv6 Services: DNS64, NAT64, content delivery ✅ High Availability: IPv6 service announcements with health checks ✅ Traffic Engineering: Controlling IPv6 traffic with BGP attributes ✅ IPv6 Migration: Transitioning from IPv4 to IPv6 ✅ IoT and Mobile: 5G networks, IoT device addressing

Not for:

  • ❌ L2 VPN services (use EVPN)
  • ❌ L3 VPN services (use VPNv6)
  • ❌ Multicast routing (use IPv6 Multicast SAFI)
  • ❌ Traffic filtering (use FlowSpec IPv6)

IPv6 vs IPv4 Differences

Address Length

Feature IPv4 IPv6
Address Length 32 bits 128 bits
Notation Dotted decimal (192.0.2.1) Colon-hexadecimal (2001:db8::1)
Address Space ~4.3 billion 340 undecillion
Prefix Example 192.0.2.0/24 2001:db8::/32

BGP Protocol Differences

Feature IPv4 Unicast IPv6 Unicast
AFI/SAFI AFI 1, SAFI 1 AFI 2, SAFI 1
BGP Encoding Traditional UPDATE MP-BGP (MP_REACH_NLRI)
Next-Hop IPv4 address IPv6 address (link-local or global)
Address Family Default in BGP Must be explicitly enabled

Key IPv6 Features

  • No NAT Required: End-to-end addressability
  • No Broadcast: Uses multicast for neighbor discovery
  • Link-Local Addresses: Every interface has link-local (fe80::/10)
  • Global Unicast Addresses: Public IPv6 addresses (2000::/3)

Configuration

Basic IPv6 Unicast Configuration

# /etc/exabgp/ipv6-unicast.conf

neighbor 2001:db8::1 {
    router-id 192.0.2.2;        # Router ID is still IPv4 format
    local-address 2001:db8::2;  # Local IPv6 address
    local-as 65001;
    peer-as 65000;

    # Enable IPv6 unicast
    family {
        ipv6 unicast;
    }

    # API process for dynamic route announcements
    api {
        processes [ ipv6-route-injector ];
    }
}

process ipv6-route-injector {
    run python3 /etc/exabgp/announce-ipv6.py;
    encoder text;
}

Dual-Stack Configuration (IPv4 + IPv6)

# Run both IPv4 and IPv6 over the same BGP session

neighbor 192.0.2.1 {
    router-id 192.0.2.2;
    local-address 192.0.2.2;  # IPv4 transport
    local-as 65001;
    peer-as 65000;

    # Enable both IPv4 and IPv6 unicast
    family {
        ipv4 unicast;
        ipv6 unicast;
    }

    api {
        processes [ dual-stack-injector ];
    }
}

IPv6 Route Announcement Script

#!/usr/bin/env python3
# /etc/exabgp/announce-ipv6.py

import sys
import time

def announce_ipv6_route(prefix, nexthop="self", **attributes):
    """Announce IPv6 unicast route"""
    cmd = f"announce route {prefix} next-hop {nexthop}"

    # Add optional attributes
    if 'local_preference' in attributes:
        cmd += f" local-preference {attributes['local_preference']}"
    if 'med' in attributes:
        cmd += f" med {attributes['med']}"
    if 'community' in attributes:
        communities = ' '.join(attributes['community'])
        cmd += f" community [ {communities} ]"

    print(cmd)
    sys.stdout.flush()

def withdraw_ipv6_route(prefix, nexthop="self"):
    """Withdraw IPv6 unicast route"""
    print(f"withdraw route {prefix} next-hop {nexthop}")
    sys.stdout.flush()

# Example: Announce IPv6 service prefix
announce_ipv6_route(
    "2001:db8:100::/48",
    nexthop="self",
    local_preference=200,
    community=["65001:100"]
)

# Keep process running
while True:
    time.sleep(60)

API Examples

Announce Routes

Basic IPv6 Route Announcement

# Simplest form (using 'self' for next-hop)
print("announce route 2001:db8::/32 next-hop self")
sys.stdout.flush()

# Explicit IPv6 next-hop
print("announce route 2001:db8::/32 next-hop 2001:db8::1")
sys.stdout.flush()

# /128 host route (anycast service IP)
print("announce route 2001:db8:100::1/128 next-hop self")
sys.stdout.flush()

# Common IPv6 prefix allocations
print("announce route 2001:db8:1000::/36 next-hop self")  # /36 allocation
print("announce route 2001:db8:2000::/48 next-hop self")  # /48 site prefix
print("announce route 2001:db8:3000::/64 next-hop self")  # /64 subnet
sys.stdout.flush()

IPv6 Route with Attributes

# With LOCAL_PREF and MED
print("announce route 2001:db8::/32 "
      "next-hop self "
      "local-preference 200 "
      "med 50")
sys.stdout.flush()

# With communities
print("announce route 2001:db8::/32 "
      "next-hop self "
      "community [ 65001:100 65001:200 ]")
sys.stdout.flush()

# With AS-PATH prepending
print("announce route 2001:db8::/32 "
      "next-hop self "
      "as-path [ 65001 65001 65001 ]")  # Prepend own AS 3 times
sys.stdout.flush()

# Complete example
print("announce route 2001:db8::/32 "
      "next-hop self "
      "local-preference 200 "
      "med 50 "
      "community [ 65001:100 ] "
      "as-path [ 65001 ] "
      "origin igp")
sys.stdout.flush()

IPv6 with Extended and Large Communities

# With large communities (recommended for IPv6)
print("announce route 2001:db8::/32 "
      "next-hop self "
      "large-community [ 65001:100:200 ]")
sys.stdout.flush()

# With extended communities
print("announce route 2001:db8::/32 "
      "next-hop self "
      "extended-community [ origin:65001:100 ]")
sys.stdout.flush()

Withdraw Routes

# Withdraw IPv6 route (must match announcement exactly)
print("withdraw route 2001:db8::/32 next-hop self")
sys.stdout.flush()

# Withdraw with explicit next-hop
print("withdraw route 2001:db8::/32 next-hop 2001:db8::1")
sys.stdout.flush()

Receive Routes

Configure ExaBGP to receive IPv6 routes from peer:

neighbor 2001:db8::1 {
    # ... basic config ...

    api {
        processes [ ipv6-route-receiver ];
        receive {
            parsed;
            update;
        }
    }
}

process ipv6-route-receiver {
    run python3 /etc/exabgp/receive-ipv6.py;
    encoder json;
}

IPv6 Receiver Script:

#!/usr/bin/env python3
import sys
import json

while True:
    line = sys.stdin.readline()
    if not line:
        break

    try:
        msg = json.loads(line)

        if msg.get('type') != 'update':
            continue

        update = msg.get('neighbor', {}).get('message', {}).get('update', {})

        # Process announced IPv6 routes
        if 'announce' in update:
            if 'ipv6 unicast' in update['announce']:
                routes = update['announce']['ipv6 unicast']
                for next_hop, prefixes in routes.items():
                    for prefix, attributes in prefixes.items():
                        med = attributes.get('med', 'N/A')
                        local_pref = attributes.get('local-preference', 'N/A')
                        print(f"[ANNOUNCE] {prefix} via {next_hop} "
                              f"(LP: {local_pref}, MED: {med})",
                              file=sys.stderr)

        # Process withdrawn IPv6 routes
        if 'withdraw' in update:
            if 'ipv6 unicast' in update['withdraw']:
                routes = update['withdraw']['ipv6 unicast']
                for prefix in routes:
                    print(f"[WITHDRAW] {prefix}", file=sys.stderr)

    except json.JSONDecodeError as e:
        print(f"JSON parse error: {e}", file=sys.stderr)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)

BGP Attributes

IPv6 unicast supports all the same BGP attributes as IPv4:

  • ORIGIN: IGP, EGP, INCOMPLETE
  • AS-PATH: AS path traversed
  • NEXT-HOP: IPv6 next-hop address
  • LOCAL_PREF: iBGP preference (higher = better)
  • MED: External AS entry point preference (lower = better)
  • COMMUNITY: 32-bit route tags
  • LARGE_COMMUNITY: 96-bit route tags (recommended for IPv6)
  • EXTENDED_COMMUNITY: 64-bit extended attributes
  • AGGREGATOR: Aggregation information
  • ATOMIC_AGGREGATE: Route aggregation indicator

Syntax is identical to IPv4, only the prefix format differs:

# IPv4
print("announce route 192.0.2.0/24 next-hop self med 50")

# IPv6 (same syntax, different prefix)
print("announce route 2001:db8::/32 next-hop self med 50")

Use Cases

1. IPv6 Anycast DNS Service

Scenario: Multiple DNS servers announce the same IPv6 anycast address.

#!/usr/bin/env python3
import sys
import subprocess
import time

DNS_ANYCAST_IP = "2001:db8:53::1/128"

def check_dns_health():
    """Check if DNS service is healthy"""
    try:
        result = subprocess.run(
            ['dig', '@::1', 'example.com', '+short'],
            timeout=2,
            capture_output=True
        )
        return result.returncode == 0
    except:
        return False

def announce():
    print(f"announce route {DNS_ANYCAST_IP} next-hop self")
    sys.stdout.flush()

def withdraw():
    print(f"withdraw route {DNS_ANYCAST_IP} next-hop self")
    sys.stdout.flush()

announced = False

while True:
    healthy = check_dns_health()

    if healthy and not announced:
        announce()
        announced = True
        print(f"[INFO] DNS healthy, announced {DNS_ANYCAST_IP}", file=sys.stderr)
    elif not healthy and announced:
        withdraw()
        announced = False
        print(f"[WARN] DNS unhealthy, withdrew {DNS_ANYCAST_IP}", file=sys.stderr)

    time.sleep(5)

2. Dual-Stack Service High Availability

Scenario: Announce both IPv4 and IPv6 addresses for a service.

#!/usr/bin/env python3
import sys
import time

SERVICE_IPv4 = "100.64.1.1/32"
SERVICE_IPv6 = "2001:db8:100::1/128"

def check_service_health():
    # Check service health (HTTP, TCP, etc.)
    # Return True if healthy
    return True

def announce_both():
    print(f"announce route {SERVICE_IPv4} next-hop self")
    print(f"announce route {SERVICE_IPv6} next-hop self")
    sys.stdout.flush()

def withdraw_both():
    print(f"withdraw route {SERVICE_IPv4} next-hop self")
    print(f"withdraw route {SERVICE_IPv6} next-hop self")
    sys.stdout.flush()

announced = False

while True:
    if check_service_health() and not announced:
        announce_both()
        announced = True
    elif not check_service_health() and announced:
        withdraw_both()
        announced = False

    time.sleep(10)

3. IPv6 Load Balancing with MED

Scenario: Distribute traffic across multiple IPv6-enabled servers using MED.

#!/usr/bin/env python3
import sys
import psutil
import time

SERVICE_PREFIX = "2001:db8:100::/48"

def get_load_metric():
    """Calculate MED based on CPU load"""
    cpu_percent = psutil.cpu_percent(interval=1)
    return int(100 + (cpu_percent * 4))

def announce_with_load():
    med = get_load_metric()
    print(f"announce route {SERVICE_PREFIX} "
          f"next-hop self "
          f"med {med}")
    sys.stdout.flush()
    print(f"[INFO] Announced {SERVICE_PREFIX} with MED {med}", file=sys.stderr)

while True:
    announce_with_load()
    time.sleep(10)

4. IPv6 Prefix Delegation and Aggregation

Scenario: Announce aggregated IPv6 prefix, suppress more-specifics.

# Announce aggregate /32
print("announce route 2001:db8::/32 "
      "next-hop self "
      "atomic-aggregate "
      "aggregator 2001:db8::1:65001")
sys.stdout.flush()

# Suppress more-specific /48 prefixes with NO_ADVERTISE
for i in range(1, 10):
    print(f"announce route 2001:db8:{i:x}000::/48 "
          f"next-hop self "
          f"community [ no-advertise ]")
sys.stdout.flush()

5. IPv6 Traffic Engineering with AS-PATH

Scenario: Control inbound IPv6 traffic using AS-PATH prepending.

# Primary path (short AS-PATH)
print("announce route 2001:db8::/32 "
      "next-hop self "
      "as-path [ 65001 ]")
sys.stdout.flush()

# Backup path (longer AS-PATH via prepending)
print("announce route 2001:db8::/32 "
      "next-hop self "
      "as-path [ 65001 65001 65001 65001 ] "
      "community [ 65001:backup ]")
sys.stdout.flush()

IPv6 Next-Hop Considerations

Link-Local vs Global Unicast Next-Hop

IPv6 BGP can use two types of next-hop addresses:

Link-Local Next-Hop (fe80::/10):

  • Common in eBGP sessions on directly connected links
  • Requires interface specification (not supported by all implementations)
  • ExaBGP typically uses global unicast next-hop

Global Unicast Next-Hop (2000::/3):

  • Recommended for ExaBGP
  • Works across non-directly-connected peers
  • More compatible

⚠️ Warning: next-hop self is an EXPERIMENTAL feature. Always use explicit next-hop IP addresses in production.

Best Practice: Use explicit global unicast IPv6 next-hop addresses.

# Recommended: Explicit global unicast next-hop
print("announce route 2001:db8::/32 next-hop 2001:db8::1")
sys.stdout.flush()

IPv4-Mapped IPv6 Next-Hop (RFC 5549)

Some implementations support advertising IPv4 prefixes with IPv6 next-hop. ExaBGP supports this via RFC 5549 (Extended Next-Hop capability).


Common Errors and Solutions

Error: "Invalid IPv6 address format"

Cause: Incorrect IPv6 address notation.

Solution: Use proper IPv6 format with colons, zero compression allowed.

# Incorrect
print("announce route 2001:db8:0:0:0:0:0:0/32 next-hop self")  # Not compressed

# Correct (compressed)
print("announce route 2001:db8::/32 next-hop self")

# Also correct (fully expanded)
print("announce route 2001:0db8:0000:0000:0000:0000:0000:0000/32 next-hop self")

Error: "IPv6 unicast family not enabled"

Cause: Peer router doesn't have IPv6 unicast address family configured.

Solution: Enable IPv6 unicast on both ExaBGP and peer.

Cisco IOS-XR:

router bgp 65000
 neighbor 2001:db8::2
  address-family ipv6 unicast  ← Enable IPv6
  !
 !
!

Juniper Junos:

protocols {
    bgp {
        group exabgp {
            family inet6 {  ← Enable IPv6
                unicast;
            }
        }
    }
}

Error: "Next-hop unreachable"

Cause: IPv6 next-hop address is not in a connected network.

Solution: Ensure next-hop is reachable. Use next-hop self with proper local-address.

neighbor 2001:db8::1 {
    local-address 2001:db8::2;  # Must be configured on local interface
    # ...
}

Error: "Routes announced but not in peer's RIB"

Cause: Peer may prefer another path (BGP decision process) or filter is blocking.

Solution: Check BGP attributes and peer's import policies.

Error: "Cannot establish BGP session over IPv6"

Cause: IPv6 connectivity issue between ExaBGP and peer.

Solution: Verify IPv6 connectivity:

# Test IPv6 reachability
ping6 2001:db8::1

# Check IPv6 routing
ip -6 route show

# Verify IPv6 address on interface
ip -6 addr show

Best Practices

1. Use Proper IPv6 Prefix Lengths

Common IPv6 prefix allocations:

# ISP allocation: /32
print("announce route 2001:db8::/32 next-hop self")

# Large organization: /36 or /40
print("announce route 2001:db8::/36 next-hop self")

# Site/customer: /48
print("announce route 2001:db8:1000::/48 next-hop self")

# Subnet: /64 (typical LAN size)
print("announce route 2001:db8:1000:1::/64 next-hop self")

# Host/anycast: /128
print("announce route 2001:db8::1/128 next-hop self")

2. Implement Health Checks (Same as IPv4)

Always verify service before announcing:

rise_count = 0
fall_count = 0
announced = False

while True:
    if check_ipv6_service_health():
        rise_count += 1
        fall_count = 0
        if rise_count >= 3 and not announced:
            announce()
            announced = True
    else:
        fall_count += 1
        rise_count = 0
        if fall_count >= 2 and announced:
            withdraw()
            announced = False

    time.sleep(5)

3. Use Global Unicast Addresses

Avoid link-local addresses for BGP next-hop:

# Good: Global unicast
print("announce route 2001:db8::/32 next-hop 2001:db8::1")

# Bad: Link-local (may not work)
# print("announce route 2001:db8::/32 next-hop fe80::1")

4. Dual-Stack Deployment

Run IPv4 and IPv6 together for maximum reach:

def announce_dual_stack():
    # Announce both IPv4 and IPv6
    print("announce route 100.64.1.1/32 next-hop self")
    print("announce route 2001:db8::1/128 next-hop self")
    sys.stdout.flush()

5. Use Large Communities for IPv6

Large communities (96-bit) are better suited for IPv6 networks with large ASNs:

# Standard community (limited to 2-byte AS)
print("announce route 2001:db8::/32 "
      "next-hop self "
      "community [ 65001:100 ]")

# Large community (supports 4-byte AS)
print("announce route 2001:db8::/32 "
      "next-hop self "
      "large-community [ 4200000000:100:200 ]")
sys.stdout.flush()

Important Considerations

ExaBGP Does Not Manipulate RIB/FIB

⚠️ CRITICAL: ExaBGP is a BGP protocol engine. It does NOT:

  • Install IPv6 routes in the Linux kernel routing table
  • Configure IPv6 addresses on interfaces
  • Forward IPv6 traffic
  • Handle IPv6 neighbor discovery

What ExaBGP DOES:

  • ✅ Send/receive BGP UPDATE messages with IPv6 unicast routes
  • ✅ Provide API for applications to control BGP announcements
  • ✅ Handle BGP session management

To Forward IPv6 Traffic:

  1. Configure IPv6 address on interface: ip -6 addr add 2001:db8::1/128 dev lo
  2. ExaBGP announces route via BGP: Router receives route
  3. Router installs route in RIB/FIB (if best path)
  4. Router forwards IPv6 traffic based on its routing table

IPv6 BGP Session Transport

BGP sessions can run over IPv4 or IPv6 transport:

IPv6 Transport (recommended for IPv6 routes):

neighbor 2001:db8::1 {
    local-address 2001:db8::2;  # IPv6 transport
    # Announce IPv6 routes
}

IPv4 Transport (also works):

neighbor 192.0.2.1 {
    local-address 192.0.2.2;  # IPv4 transport
    family {
        ipv6 unicast;  # Can carry IPv6 routes over IPv4 transport
    }
}

Router ID is Always IPv4 Format

Even in pure IPv6 deployments, BGP router-id is always in IPv4 dotted-decimal format:

neighbor 2001:db8::1 {
    router-id 192.0.2.2;        # IPv4 format (required)
    local-address 2001:db8::2;  # IPv6 address
}

Tip: If no IPv4 address exists, use a private IPv4 address (e.g., 10.0.0.1) for router-id.


See Also

ExaBGP Documentation

Use Cases

Operations

Getting Started


References

RFCs

ExaBGP Resources

IPv6 Resources

  • IPv6 Address Allocation: IANA IPv6 address space registry
  • IPv6 Deployment Guide: Best practices for IPv6 migration

Clone this wiki locally