diff --git a/core/register.go b/core/register.go index 0ea16de5b6..998c1cade3 100644 --- a/core/register.go +++ b/core/register.go @@ -15,6 +15,7 @@ import ( clabnodescisco_sdwan "github.com/srl-labs/containerlab/nodes/cisco_sdwan" clabnodescisco_vios "github.com/srl-labs/containerlab/nodes/cisco_vios" clabnodescjunosevolved "github.com/srl-labs/containerlab/nodes/cjunosevolved" + clabnodescodelaboratory_bng "github.com/srl-labs/containerlab/nodes/codelaboratory_bng" clabnodescrpd "github.com/srl-labs/containerlab/nodes/crpd" clabnodescvx "github.com/srl-labs/containerlab/nodes/cvx" clabnodesdell_sonic "github.com/srl-labs/containerlab/nodes/dell_sonic" @@ -117,4 +118,5 @@ func (c *CLab) RegisterNodes() { //nolint:funlen clabnodesvyosnetworks_vyos.Register(c.Reg) clabnodescjunosevolved.Register(c.Reg) clabnodesarrcus_arcos.Register(c.Reg) + clabnodescodelaboratory_bng.Register(c.Reg) } diff --git a/docs/lab-examples/bng01.md b/docs/lab-examples/bng01.md new file mode 100644 index 0000000000..ee30bc625d --- /dev/null +++ b/docs/lab-examples/bng01.md @@ -0,0 +1,75 @@ +| | | +| ----------------------------- | -------------------------------------------------------------------------------------- | +| **Description** | BNG with subscriber simulator and core router | +| **Components** | [Code Laboratory BNG][bng], [BNG Blaster][blaster], [FRR][frr] | +| **Resource requirements**[^1] | :fontawesome-solid-microchip: 2
:fontawesome-solid-memory: 2 GB | +| **Topology file** | [bng01.clab.yml][topofile] | +| **Name** | bng01 | + +## Description + +This lab demonstrates Code Laboratory's eBPF/XDP-based BNG handling IPoE/DHCPv4 subscriber sessions. The topology consists of three nodes: + +| Node | Role | Image | +|------|------|-------| +| `bng1` | Broadband Network Gateway | `ghcr.io/codelaboratoryltd/bng:latest` | +| `subscribers` | Subscriber simulator (BNG Blaster) | `veesixnetworks/bngblaster:0.9.30` | +| `corerouter1` | Core/upstream router (FRR) | `frrouting/frr:v8.4.1` | + +## Topology + +``` +subscribers:eth1 <-----> bng1:eth1 (access) + bng1:eth2 (core) <-----> corerouter1:eth1 +``` + +## Deploying the lab + +```bash +sudo clab deploy -t lab-examples/bng01/bng01.clab.yml +``` + +## Verification + +Check BNG is running: + +```bash +docker logs clab-bng01-bng1 +``` + +Check FRR routing: + +```bash +docker exec clab-bng01-corerouter1 vtysh -c "show ip route" +``` + +Run BNG Blaster to simulate 10 untagged IPoE/DHCPv4 subscribers: + +```bash +docker exec -it clab-bng01-subscribers bngblaster -C /config/config.json +``` + +A QinQ (802.1ad) subscriber config is also provided for double-tagged deployments: + +```bash +docker exec -it clab-bng01-subscribers bngblaster -C /config/qinq.json +``` + +Check BNG metrics: + +```bash +curl http://clab-bng01-bng1:9090/metrics +``` + +## Cleanup + +```bash +sudo clab destroy -t lab-examples/bng01/bng01.clab.yml +``` + +[bng]: https://github.com/codelaboratoryltd/bng +[blaster]: https://github.com/rtbrick/bngblaster +[frr]: https://docs.frrouting.org/en/stable-8.4/ +[topofile]: https://github.com/srl-labs/containerlab/tree/main/lab-examples/bng01/bng01.clab.yml + +[^1]: Resource requirements are provisional. Consult with the installation guides for additional information. diff --git a/docs/manual/kinds/codelaboratory_bng.md b/docs/manual/kinds/codelaboratory_bng.md new file mode 100644 index 0000000000..9e24aefd65 --- /dev/null +++ b/docs/manual/kinds/codelaboratory_bng.md @@ -0,0 +1,167 @@ +--- +search: + boost: 4 +kind_code_name: codelaboratory_bng +kind_display_name: Code Laboratory BNG +--- +# Code Laboratory BNG + +Code Laboratory's eBPF/XDP-based Broadband Network Gateway is identified with `-{{ kind_code_name }}-` kind in the [topology file](../topo-def-file.md). + +The BNG uses eBPF/XDP for kernel-level DHCP fast path processing, achieving sub-100μs DHCP response times for cached subscribers. Unlike VPP-based BNGs, it requires only `NET_ADMIN` and `BPF` Linux capabilities — no hugepages, DPDK, or dedicated NICs. + +## Getting -{{ kind_display_name }}- image + +The -{{ kind_display_name }}- container image is available from GitHub Container Registry: + +```bash +docker pull ghcr.io/codelaboratoryltd/bng:latest +``` + +Source code and build instructions are available on [GitHub](https://github.com/codelaboratoryltd/bng). + +## Managing -{{ kind_display_name }}- nodes + +### Health check + +The BNG exposes a health endpoint on the metrics port: + +```bash +curl http://:9090/health +``` + +### Prometheus metrics + +Metrics are served at the configured metrics address (default `:9090`): + +```bash +curl http://:9090/metrics +``` + +Key metrics include DHCP fast/slow path latencies, cache hit rates, pool utilization, and active session counts. + +## Interfaces naming + +-{{ kind_display_name }}- nodes use the following interface naming convention: + +| Interface | Purpose | +|-----------|---------| +| `eth0` | Management (containerlab default) | +| `eth1` | Access / subscriber-facing | +| `eth2+` | Core / upstream links | + +The `eth0` interface can only be used when `network-mode` is set to `none`. + +There are no other restrictions on the interface naming besides the generic Linux interface naming rules. + +## Features and options + +### Startup configuration + +The [`startup-config`](../nodes.md#startup-config) property sets the path to a YAML config file that is mounted to `/etc/bng/config.yaml` inside the container. + +The config file uses a flat `key: value` format where each key matches a CLI flag name (without `--`). CLI flags take precedence over config file values. + +Example minimal config: + +```yaml +interface: eth1 +pool-network: 10.0.1.0/24 +pool-gateway: 10.0.1.1 +pool-dns: "8.8.8.8,8.8.4.4" +lease-time: 3600s +metrics-addr: ":9090" +log-level: info +``` + +Example with advanced features: + +```yaml +interface: eth1 +pool-network: 10.0.1.0/24 +pool-gateway: 10.0.1.1 +pool-dns: "8.8.8.8,8.8.4.4" +lease-time: 3600s +metrics-addr: ":9090" +log-level: info + +# Anti-spoofing (disabled, strict, loose, log-only) +antispoof-mode: log-only + +# Walled garden captive portal +walled-garden: "false" +walled-garden-portal: "10.255.255.1:8080" + +# NAT44/CGNAT +nat-enabled: "true" +nat-inside-interface: eth1 +nat-outside-interface: eth2 + +# PPPoE +pppoe-enabled: "false" + +# HA failover (active or standby) +ha-role: "" +ha-peer: "" +``` + +### Environment variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `BNG_INTERFACE` | `eth1` | Subscriber-facing interface | +| `BNG_LOG_LEVEL` | `info` | Log level (debug, info, warn, error) | + +### Linux capabilities + +The -{{ kind_display_name }}- kind automatically adds the following capabilities: + +| Capability | Purpose | +|------------|---------| +| `NET_ADMIN` | Network interface and eBPF map management | +| `BPF` | Loading eBPF/XDP programs into the kernel | + +This is significantly lighter than VPP-based BNGs which additionally require `SYS_ADMIN`, `IPC_LOCK`, `SYS_NICE`, and `SYS_RAWIO` for DPDK hugepage management. + +### QinQ (802.1ad) support + +The BNG supports QinQ double VLAN tagging at the eBPF/XDP data plane level. Subscribers are identified by S-TAG (service VLAN, outer) and C-TAG (customer VLAN, inner) pairs, supporting up to 100,000 concurrent VLAN-based subscribers. + +The [bng01 lab example](../../lab-examples/bng01.md) includes both untagged and QinQ subscriber configs for [BNG Blaster][blaster]. + +[blaster]: https://github.com/rtbrick/bngblaster + +## Quickstart + +The following topology creates a minimal BNG lab with a subscriber simulator and core router: + +```yaml +name: bng-quickstart + +topology: + nodes: + bng1: + kind: codelaboratory_bng + image: ghcr.io/codelaboratoryltd/bng:latest + startup-config: bng1/config.yaml + exec: + - ip addr add 10.0.1.1/24 dev eth1 + - ip addr add 10.0.0.1/24 dev eth2 + subscribers: + kind: linux + image: veesixnetworks/bngblaster:0.9.30 + binds: + - subscribers/config.json:/config/config.json + corerouter1: + kind: linux + image: frrouting/frr:v8.4.1 + binds: + - corerouter1/daemons:/etc/frr/daemons + - corerouter1/frr.conf:/etc/frr/frr.conf + + links: + - endpoints: ["subscribers:eth1", "bng1:eth1"] + - endpoints: ["bng1:eth2", "corerouter1:eth1"] +``` + +See the [bng01 lab example](../../lab-examples/bng01.md) for a complete working topology with all config files. diff --git a/docs/manual/kinds/index.md b/docs/manual/kinds/index.md index 549d594768..bd060f5cc7 100644 --- a/docs/manual/kinds/index.md +++ b/docs/manual/kinds/index.md @@ -76,6 +76,7 @@ Within each predefined kind, we store the necessary information that is used to | **6WIND VSR** | [`6wind_vsr`](6wind_vsr.md) | supported | container | | **Keysight ixia-c-one** | [`keysight_ixia-c-one`](keysight_ixia-c-one.md) | supported | container | | **Arrcus ArcOS** | [`arrcus_arcos`](arrcus_arcos.md) | supported | container | +| **Code Laboratory BNG** | [`codelaboratory_bng`](codelaboratory_bng.md) | supported | container | | **FD.io VPP** | [`fdio_vpp`](fdio_vpp.md) | supported | container | | **RARE/freeRtr** | [`rare`](rare-freertr.md) | supported | container | | **VyOS Networks VyOS** | [`vyosnetworks_vyos`](vyosnetworks_vyos.md) | supported | VM | diff --git a/lab-examples/bng01/bng01.clab.yml b/lab-examples/bng01/bng01.clab.yml new file mode 100644 index 0000000000..4f468ba451 --- /dev/null +++ b/lab-examples/bng01/bng01.clab.yml @@ -0,0 +1,27 @@ +name: bng01 + +topology: + nodes: + bng1: + kind: codelaboratory_bng + image: ghcr.io/codelaboratoryltd/bng:latest + startup-config: bng1/config.yaml + exec: + - ip addr add 10.0.1.1/24 dev eth1 + - ip addr add 10.0.0.1/24 dev eth2 + corerouter1: + kind: linux + image: frrouting/frr:v8.4.1 + binds: + - corerouter1/daemons:/etc/frr/daemons + - corerouter1/frr.conf:/etc/frr/frr.conf + subscribers: + kind: linux + image: veesixnetworks/bngblaster:0.9.30 + binds: + - subscribers/config.json:/config/config.json + - subscribers/qinq.json:/config/qinq.json + + links: + - endpoints: ["subscribers:eth1", "bng1:eth1"] + - endpoints: ["bng1:eth2", "corerouter1:eth1"] diff --git a/lab-examples/bng01/bng1/config.yaml b/lab-examples/bng01/bng1/config.yaml new file mode 100644 index 0000000000..8d5d758eb5 --- /dev/null +++ b/lab-examples/bng01/bng1/config.yaml @@ -0,0 +1,12 @@ +# BNG startup configuration for containerlab +# +# Flat key=value format where each key matches a CLI flag name (without --). +# CLI flags take precedence over values in this file. + +interface: eth1 +pool-network: 10.0.1.0/24 +pool-gateway: 10.0.1.1 +pool-dns: "8.8.8.8,8.8.4.4" +lease-time: 3600s +metrics-addr: ":9090" +log-level: info diff --git a/lab-examples/bng01/corerouter1/daemons b/lab-examples/bng01/corerouter1/daemons new file mode 100644 index 0000000000..a0cc2b5bf8 --- /dev/null +++ b/lab-examples/bng01/corerouter1/daemons @@ -0,0 +1,22 @@ +zebra=yes +bgpd=no +ospfd=yes +ospf6d=no +ripd=no +ripngd=no +isisd=no +pimd=no +ldpd=no +nhrpd=no +eigrpd=no +babeld=no +sharpd=no +staticd=yes +pbrd=no +bfdd=no +fabricd=no + +vtysh_enable=yes +zebra_options=" -A 127.0.0.1" +ospfd_options=" -A 127.0.0.1" +staticd_options="-A 127.0.0.1" diff --git a/lab-examples/bng01/corerouter1/frr.conf b/lab-examples/bng01/corerouter1/frr.conf new file mode 100644 index 0000000000..731330713d --- /dev/null +++ b/lab-examples/bng01/corerouter1/frr.conf @@ -0,0 +1,28 @@ +frr version 8.4.1 +frr defaults traditional +hostname corerouter1 +log syslog informational +service integrated-vtysh-config +! +interface eth1 + ip address 10.0.0.2/24 + ip ospf area 0.0.0.0 + ip ospf network point-to-point +exit +! +interface lo + ip address 10.255.0.2/32 + ip ospf area 0.0.0.0 + ip ospf passive +exit +! +router ospf + ospf router-id 10.255.0.2 + log-adjacency-changes + redistribute connected +exit +! +ip route 10.0.1.0/24 10.0.0.1 +! +line vty +! diff --git a/lab-examples/bng01/subscribers/config.json b/lab-examples/bng01/subscribers/config.json new file mode 100644 index 0000000000..a904c6dcfa --- /dev/null +++ b/lab-examples/bng01/subscribers/config.json @@ -0,0 +1,26 @@ +{ + "interfaces": { + "access": [ + { + "interface": "eth1", + "type": "ipoe", + "vlan-mode": "N:1" + } + ] + }, + "dhcp": { + "enable": true + }, + "ipoe": { + "ipv4": true, + "ipv6": false + }, + "sessions": { + "count": 10, + "start-rate": 2 + }, + "access-line": { + "agent-circuit-id": "eth 0/1/{session-global}", + "agent-remote-id": "BNG-Blaster-{session-global}" + } +} diff --git a/lab-examples/bng01/subscribers/qinq.json b/lab-examples/bng01/subscribers/qinq.json new file mode 100644 index 0000000000..601b34faaf --- /dev/null +++ b/lab-examples/bng01/subscribers/qinq.json @@ -0,0 +1,30 @@ +{ + "interfaces": { + "access": [ + { + "interface": "eth1", + "type": "ipoe", + "vlan-mode": "1:1", + "outer-vlan-min": 100, + "outer-vlan-max": 100, + "inner-vlan-min": 10, + "inner-vlan-max": 19 + } + ] + }, + "dhcp": { + "enable": true + }, + "ipoe": { + "ipv4": true, + "ipv6": false + }, + "sessions": { + "count": 10, + "start-rate": 2 + }, + "access-line": { + "agent-circuit-id": "eth 0/1/{session-global}:100.{session-global}", + "agent-remote-id": "BNG-Blaster-{session-global}" + } +} diff --git a/mkdocs.yml b/mkdocs.yml index 8f1e091a59..eeebe8ed87 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,7 @@ nav: - Palo Alto PAN: manual/kinds/vr-pan.md - 6WIND VSR: manual/kinds/6wind_vsr.md - Arrcus ArcOS: manual/kinds/arrcus_arcos.md + - Code Laboratory BNG: manual/kinds/codelaboratory_bng.md - FD.io VPP: manual/kinds/fdio_vpp.md - KinD: manual/kinds/k8s-kind.md - Linux bridge: manual/kinds/bridge.md @@ -177,6 +178,7 @@ nav: - Cisco ASAv: lab-examples/asav01.md - Cisco FTDv: lab-examples/ftdv01.md - F5 BIG-IP VE: lab-examples/f5bigipve01.md + - Code Laboratory BNG: lab-examples/bng01.md - Templated labs: - Leaf-spine topology: lab-examples/templated01.md - 5-stage Clos topology: lab-examples/templated02.md diff --git a/nodes/codelaboratory_bng/codelaboratory_bng.go b/nodes/codelaboratory_bng/codelaboratory_bng.go new file mode 100644 index 0000000000..8dc04c9351 --- /dev/null +++ b/nodes/codelaboratory_bng/codelaboratory_bng.go @@ -0,0 +1,137 @@ +// Copyright 2025 Code Laboratory Ltd +// Licensed under the BSD 3-Clause License. +// SPDX-License-Identifier: BSD-3-Clause + +package codelaboratory_bng + +import ( + "context" + "fmt" + "os" + "path" + "strings" + + clabconstants "github.com/srl-labs/containerlab/constants" + clabnodes "github.com/srl-labs/containerlab/nodes" + clabtypes "github.com/srl-labs/containerlab/types" + clabutils "github.com/srl-labs/containerlab/utils" +) + +const ( + generateable = true + generateIfFormat = "eth%d" + + bngCfgDstPath = "/etc/bng/config.yaml" + ifWaitScriptDstPath = "/usr/sbin/if-wait.sh" +) + +var kindNames = []string{"codelaboratory_bng"} + +// Register registers the node in the NodeRegistry. +func Register(r *clabnodes.NodeRegistry) { + generateNodeAttributes := clabnodes.NewGenerateNodeAttributes(generateable, generateIfFormat) + nrea := clabnodes.NewNodeRegistryEntryAttributes(nil, generateNodeAttributes, nil) + + r.Register(kindNames, func() clabnodes.Node { + return new(codelaboratory_bng) + }, nrea) +} + +type codelaboratory_bng struct { + clabnodes.DefaultNode + // Path to the BNG config file on the host + bngCfgSrcPath string + // Path to the interface wait script on the host + ifWaitSrcPath string +} + +func (n *codelaboratory_bng) Init(cfg *clabtypes.NodeConfig, opts ...clabnodes.NodeOption) error { + // Init DefaultNode + n.DefaultNode = *clabnodes.NewDefaultNode(n) + n.Cfg = cfg + + // Capabilities required by BNG (eBPF/XDP - much lighter than VPP/DPDK) + n.Cfg.CapAdd = append(n.Cfg.CapAdd, + "NET_ADMIN", + "BPF", + ) + + // Tell the BNG which interface to use for subscriber traffic + if n.Cfg.Env == nil { + n.Cfg.Env = make(map[string]string) + } + + // Set up the BNG config bind mount + n.bngCfgSrcPath = path.Join(n.Cfg.LabDir, "config.yaml") + n.Cfg.ResStartupConfig = n.bngCfgSrcPath + n.Cfg.Binds = append(n.Cfg.Binds, fmt.Sprint(n.Cfg.ResStartupConfig, ":", bngCfgDstPath)) + + // Bind-mount the interface wait script so the container can wait for + // containerlab to wire the veth links before starting the BNG process. + n.ifWaitSrcPath = path.Join(n.Cfg.LabDir, "if-wait.sh") + n.Cfg.Binds = append(n.Cfg.Binds, fmt.Sprint(n.ifWaitSrcPath, ":", ifWaitScriptDstPath)) + + // Override the entrypoint to wait for interfaces before starting BNG. + // The BNG image has ENTRYPOINT ["/app/bng"] CMD ["run"], but containerlab + // creates containers first and wires links afterwards. Without this wrapper, + // the BNG process tries to bind its subscriber-facing interface (eth1) + // before the veth pair exists and crashes. + n.Cfg.Entrypoint = "sh -c '" + ifWaitScriptDstPath + " ; exec /app/bng run --config " + bngCfgDstPath + "'" + + for _, o := range opts { + o(n) + } + + return nil +} + +func (n *codelaboratory_bng) PreDeploy(_ context.Context, params *clabnodes.PreDeployParams) error { + clabutils.CreateDirectory(n.Cfg.LabDir, clabconstants.PermissionsOpen) + + // Generate the interface wait script from the shared template. + // This script polls /sys/class/net/ until all CLAB_INTFS interfaces + // appear, ensuring the BNG doesn't start before its links are wired. + clabutils.CreateFile(n.ifWaitSrcPath, clabutils.IfWaitScript) + os.Chmod(n.ifWaitSrcPath, clabconstants.PermissionsOpen) + + nodeCfg := n.Config() + + // Handle startup config provided by the user + var bngCfgTpl string + + if nodeCfg.StartupConfig != "" { + c, err := os.ReadFile(nodeCfg.StartupConfig) + if err != nil { + return err + } + bngCfgTpl = string(c) + } + + err := n.GenerateConfig(n.Cfg.ResStartupConfig, bngCfgTpl) + if err != nil { + return err + } + + return nil +} + +// CheckInterfaceName allows any interface name for BNG nodes, but checks +// if eth0 is only used with network-mode=none. +func (n *codelaboratory_bng) CheckInterfaceName() error { + nm := strings.ToLower(n.Cfg.NetworkMode) + for _, e := range n.Endpoints { + if e.GetIfaceName() == "eth0" && nm != "none" { + return fmt.Errorf( + "eth0 interface name is not allowed for %s node when network mode is not set to none", + n.Cfg.ShortName, + ) + } + } + return nil +} + +func (n *codelaboratory_bng) GetImages(_ context.Context) map[string]string { + return map[string]string{ + clabnodes.ImageKey: n.Cfg.Image, + } +}