Homepage
Privacy Policy
iYoRoy DN42 Network
About
More
Friends
Language
简体中文
English
Search
1
Centralized Deployment of EasyTier using Docker
1,705 Views
2
Adding KernelSU Support to Android 4.9 Kernel
1,091 Views
3
Enabling EROFS Support for an Android ROM with Kernel 4.9
309 Views
4
Installing 1Panel Using Docker on TrueNAS
300 Views
5
2025 Yangcheng Cup CTF Preliminary WriteUp
296 Views
Android
Ops
NAS
Develop
Network
Projects
DN42
One Man ISP
CTF
Kubernetes
Cybersecurity
Brain Dumps
IoT
Login
Search
Search Tags
BGP
Network Technology
BIRD
Linux
DN42
iBGP
Android
C&C++
OSPF
Cybersecurity
Clearnet
CTF
MSVC
AOSP
Web
Docker
Windows
Kernel
IGP
TrueNAS
Kagura iYoRoy
A total of
36
articles have been written.
A total of
23
comments have been received.
Index
Column
Android
Ops
NAS
Develop
Network
Projects
DN42
One Man ISP
CTF
Kubernetes
Cybersecurity
Brain Dumps
IoT
Pages
Privacy Policy
iYoRoy DN42 Network
About
Friends
Language
简体中文
English
9
articles related to
were found.
DN42&OneManISP – Confederation, Automation, IPAM and Overlay Network
This article does not go into implementation details in depth; it only records the internal network architecture design process and automation ideas. If you need reference code or configuration files, please see iYoRoy-Network/bird2-config: BIRD2 Configuration for iYoRoy Network (AS4242422024, AS205369). The repository contains a significant amount of AI-generated code. This article only borrows the logical layering concept of Underlay/Overlay (base layer/business layer), specifically referring to the separation of address roles, and does not involve tunneling encapsulation technologies such as VXLAN/GRE. TL;DR The entire internal network architecture was refactored. What was originally a horizontally split network based on “DN42 / IANA / WireGuard / BIRD config files” was transformed into a set of infrastructure organised by network intent. The final design can be summarised as: Use WireGuard to carry underlay transport between nodes; Use BGP Confederation instead of OSPF / full-mesh iBGP as the internal routing backbone; Separate node identity and service addresses with Underlay / Overlay; Automatically derive AS, loopback and link-local addresses from Tier / Region / Node ID / Token; Use BGP Large Community to express route origin, propagation scope and export policy; Use Ansible to compile high-level peer intent into WireGuard + BIRD + interface configurations and deploy them automatically. Background In some earlier articles from these two series, we successfully brought up a DN42 AS and an IANA AS, with the same infrastructure carrying traffic for both networks simultaneously. In a previous post, DN42&OneManISP - Troubleshooting OSPF Source Address in a Coexistence Environment - iYoRoy's Develop Diary, there was already some initial separation and isolation for the two networks. However, as the number of nodes grew, manually maintaining the WireGuard and BIRD configuration files for this whole thing became more and more complex. Moreover, public network nodes also needed to interconnect and provide mutual IP transit, so in the end I decided to do a thorough refactoring of this entire internal network. Analysis Problems with the Old Architecture Before the refactoring, DN42 routes between nodes were mutually propagated, using OSPF over WireGuard as the IGP, with full-mesh iBGP on top to carry full routing information between border routers. For IANA traffic, nodes were completely isolated from each other, with each node independently advertising its own /48 IANA IPv6 prefix. To sum up: WireGuard was responsible for tunnels between nodes; OSPF over WireGuard was responsible for the IGP; DN42 border routers ran full-mesh iBGP on top; IANA nodes basically each independently advertised their own public prefixes. This architecture worked well when there were few nodes, but problems started to become apparent as the number of nodes increased: WireGuard, OSPF, and BIRD configurations had to be maintained separately; Adding a new node required changes in multiple places, making it easy to miss something; Routing policies for DN42 and IANA influenced each other, yet the configuration was scattered; Cross-PoP scheduling of IANA prefixes was not natural; Some nodes were only transit nodes but were forced to have an overlay identity; Source address selection, route leaks, and exposure of internal addresses became increasingly difficult to control. Design Goals and BGP Confederation The main goals for this refactoring were: Adding nodes should be simple: ideally, only the node identity and peer relationships need to be described; DN42, IANA and the internal backbone should be able to share infrastructure while remaining policy-isolated from each other; IP addresses should be schedulable resources, not fixed identities of nodes; The internal topology should not leak to external BGP peers; Configurations should be generated from declarative data, not by handwriting lots of repetitive WireGuard/BIRD sessions; The deployment process should be automated. There were also several other, more detailed requirements: Some IANA nodes should be able to carry IP transit for other nodes, using an IANA PoP to advertise public IPs for other nodes; Currently, the IANA AS is maintained jointly by たのしい and myself; we need to coordinate, with some infrastructure needing mutual transit and route tuning; We only have one 44Net IPv4 /24 (sponsored by たのしい); if we want to advertise it, all nodes would need iBGP interconnection and an IGP, otherwise only a single PoP can use it; The IANA AS needs to accept downstreams; Route optimization is needed between IANA PoPs. The previous IANA IPv6 prefix assignment rule followed Continent 3 bits + Region 3 bits + Multi-node within region 2 bits, carving several /48s out of a complete /40. However, due to certain upstream limitations on the number of advertised prefixes, and because such fragmented prefixes are unfavourable for IPAM, we planned to eventually aggregate external advertisements only to the continent level, and then handle more detailed parts via internal iBGP routing. The internal network structure on たのしい's side is BGP Confederation, implemented with a manually simulated approach. Actually, after this analysis, defining the internal network architecture became quite straightforward: the most suitable solution was to switch the internal network to BGP Confederation as well, and then utilise the concept of "BGP as IGP" to advertise /32 and /128 routes within the confederation to handle internal routing. At the same time, because a confederation avoids the split-horizon characteristic of traditional iBGP, it doesn't require strict full-mesh, nor does it need separate iBGP instances, making it very decentralized xD. Furthermore, BGP Confederation can be easily merged with たのしい's infrastructure, requiring only that both sides treat the other's internal ASes as confederation ASes. This solution has another benefit, because the containment relationship of our infrastructure is actually like this: graph subgraph 4242423377 Infrastructure 3377_DN42_PoP[DN42 PoP] 3377_IANA_PoP[IANA PoP] end subgraph 4242422024 Infrastructure 2024_DN42_PoP[DN42 PoP] 2024_IANA_PoP[IANA PoP] end 3377_IANA_PoP <==Cross-Infra Confederation==> 2024_IANA_PoP 3377_DN42_PoP <--> DN42 <--> 2024_DN42_PoP 3377_IANA_PoP <--> IANA <--> 2024_IANA_PoP Our DN42 PoPs and transit are isolated, but the IANA PoPs and transit are interconnected. In the BGP Confederation scenario, we can use filters + BGP (Large) Community to isolate route origins while still exchanging IANA traffic engineering intents, achieving a single standard usable across both infrastructures. Overlay and IPAM In the old design, a node often simultaneously held two identities: It was a router in the network; It was also a service address holder reachable within DN42 / IANA. After the refactoring, I decided to separate these two identities: Underlay: the node's identity as a router, used for internal interconnection, next-hop, tunnels, and forwarding; Overlay: the DN42 / IANA address used when the node provides services to the outside world or accepts traffic. This way, a node can only participate in forwarding without having a DN42 IPv4 address; it can also temporarily announce a certain overlay /32 or /128 when needed. In practical application scenarios, my DN42 address space is not very abundant; I originally registered a /28, with only 16 usable addresses. Some nodes, like IEPL tunnel nodes or IX nodes, only perform forwarding roles, do not need to host services, and do not need to be connected from the outside. Therefore, theoretically they do not need to be assigned independent, reachable DN42/IANA addresses. Moreover, since we had already decided to switch the internal network to BGP Confederation, based on this configuration we could re-plan the originally horizontally split DN42 + IANA network structure into a vertical Underlay + Overlay division. DN42 and IANA addresses/traffic become the payload of the upper-layer Overlay Network, while the lower-layer Underlay Network serves as the infrastructure for underlying communication and forwarding between nodes. In this scenario, IP addresses become resources that can be easily scheduled within the internal network, rather than unique IDs bound to a node. The advantages are clear: Saves scarce address resources: transit nodes can have only an underlay identity, without needing DN42 IPv4 or public addresses; Anycast and address migration: Overlay addresses become resources schedulable via BGP, no longer tightly bound to a specific machine; Hides transit nodes: pure transit nodes do not need to expose globally reachable addresses, making it harder for outsiders to directly probe the internal topology; Easy new node onboarding: new nodes join the underlay first, and overlay addresses are assigned when they need to carry services; Elegant transit delivery: IANA PoPs can deliver customer prefixes or service addresses to other nodes via the internal confederation. Design In summary, the final design consisted of the following IPAM mechanism and automation workflow. Each node possesses some basic metadata: Tier: indicates the node's purpose, e.g., backbone, IX access, or intra-net node Region: the continent region where the node is located ID: the node's ID within that region Token: a random 16-bit string assigned to the node, used as a unique ID for the node under IPv6 For Tier and Region, several tables were created as data sources: Tier Desc 1 Backbone 2 IX 3 Backbone + IX Mixed 9 Node Region Desc 0 Reserved 1 Asia 2 Europe 3 Africa 4 North America 5 South America 6 Oceania 7 Antarctica Confederation AS: 4220240000 – 4220249999 Assignment rule: 422024{tier}{region}{node_id:02d} Where tier indicates the node's purpose, region indicates the area where the node is located, and the last two digits node_id distinguish different nodes within the same region. Underlay IPAM IPv4 Prefix: 100.64.0.0/16 Assignment rule: 100.64.{tier*10 + region}.{node_id}/32 IPv6 Prefix: fd18:3e15:61d0:ffff::/64 Assignment rule: fd18:3e15:61d0:ffff:{tier}:{region}::{node_token}/128 To simplify the automation workflow, I also intended to bring the link-local addresses between WireGuard tunnels under automated management, generating them using the aforementioned Tier, Region, and Token, with the automation scripts generating WireGuard configuration and automatically setting up the tunnel and confederation BGP session. The link-local assignment rule generally follows the normal IPv6 rule: fe80::2024:{tier}:{region}:{node_token}/64 Thus, each node only needs to be assigned its metadata to automatically generate the internal network confederation AS and addresses according to the rules above. Overlay IPAM The DN42 IPv4 part is assigned manually. This part has some historical legacy architecture, and changes need to be merged into the official registry, so I plan to assign it manually. The Underlay IPv6 is itself DN42-reachable; if other DN42 IPv6 addresses need to be allocated, they can simply be advertised on demand. For IANA IPv6, the current design format is to first calculate the continent /43 according to the initially proposed rule, e.g., for Asia: 2a14:7583:f220::/43 Then calculate the /46 based on the region, e.g., for Hong Kong region HKG: 2a14:7583:f224::/46 Next, bits from /46 to /112 are set to zero, and the last 16 bits are filled with the node Token to serve as the node's IANA IPv6 address. For example, one of my backbone nodes in Hong Kong: 2a14:7583:f224::7d89/128 Community Isolation Mechanism In this architecture, BGP Large Community effectively acts as the metadata and control information carried along with a route as it propagates between different modules and infrastructures. Based on this metadata, when we export routes to different external peers we can determine: Does it belong to DN42 or IANA? Is it locally originated, brought in by a downstream, learned from a peer, or learned from an upstream? Can it be exported to other ASes? Is it underlay-only? Does this route need AS prepending? As a concrete example, if the first identifier marks the route origin/destination as DN42 or IANA, all routes permitted to be sent to DN42 will be tagged with: (4242422024, 1, 1) Similarly, all routes destined for IANA will be tagged with: (205369, 1, 1) Within various protocols, this acts as a master switch, controlling whether export should occur. IANA upstream export filter: template bgp iana_upstream_v6 { ... ipv6 { ... import filter { if !iana_filter_default_check() then reject; # basic check remove_confederation_as(); # remove confederation as to prevent external peers from maliciously carrying internal as remove_private_community(); # remove private community to prevent external peers from maliciously manipulating internal routes iana_upstream_add_community(); # tag all routes from IANA with community permitting broadcast in IANA infrastructure accept; }; export filter { if !iana_filter_default_check() then reject; # basic check if !iana_upstream_check_community() then reject; # check community: is this a route destined for IANA / does it carry no-advertise/no-export etc. remove_confederation_as(); # remove confederation as remove_private_community(); # remove private community accept; }; ... }; ... } Where iana_filter_default_check() is used to check prefix length, ROA, whether it is a default route, and other miscellaneous content: function iana_filter_default_check(){ if net ~ [::/0] then return false; if net.len > 48 then return false; if bgp_large_community ~ [(IANA_OWNAS,1,1)] && is_self_iana_v6() then return true; if roa_check(iana_roa_v6, net, bgp_path.last) = ROA_INVALID then return false; return true; } remove_confederation_as(), as the name suggests, removes internal confederation ASes: function remove_confederation_as() { bgp_path.delete([4220240000..4220249999]); # 4242422024 Infrastructure bgp_path.delete([4233770000..4233779999]); # 4242423377 Infrastructure } remove_private_community(), as the name suggests, removes internal Communities. The current implementation is very rough and needs detailed refinement later, because some Communities should be open to downstreams, allowing them to use Communities to convey routing intents and perform optimizations, such as prepending AS path, which can be used for route tuning or traffic engineering: function remove_private_community(){ bgp_large_community.delete([(4242422024, *, *)]); bgp_large_community.delete([(205369, *, *)]); } iana_upstream_add_community(), tags all routes from upstream with the Community allowing circulation in the IANA infrastructure, as well as a route source identifier: function iana_upstream_add_community(){ bgp_large_community.add((205369,1,1)); bgp_large_community.add((205369,2,102)); } Where (205369,2,102) identifies the route as coming from an upstream. Route sources are divided into three types: Upstream, Peer, Downstream/Customer/Self (treated as customer). For these three different types of BGP sessions, the exported routes usually differ: To upstream: we need the upstream to advertise our own prefixes and our downstreams' prefixes, so all routes originating from downstream should be exported to upstream. To downstream: we need to provide network services, so we need to export all routes we know, i.e., routes from upstream, peer, downstream. To peer: peering connections are solely for both parties to access each other's networks, so we must not export each other's upstreams or peers, otherwise it becomes free transit/tunneling. Therefore, only routes from downstream are exported. Correspondingly in the current internal network, routes from peers are tagged (205369,2,101), and routes from downstreams are tagged (205369,2,100). iana_upstream_check_community() checks the route's Community, and based on the principles above, judges whether it should be advertised to upstream: function iana_upstream_check_community(){ if !(bgp_large_community ~ [(205369,1,1)]) then return false; # not permitted for broadcast in IANA infrastructure, reject if bgp_large_community ~ [(205369,65535,65282)] then return false; # no-advertise if bgp_large_community ~ [(205369,65535,65281)] then return false; # no-export if bgp_community ~ [(65535,65281)] then reject; # no-advertise if bgp_community ~ [(65535,65282)] then reject; # no-export if bgp_large_community ~ [(205369,2,0)] then return false; # from internal network, this community is used to identify routes from the internal network that should not be advertised externally, reject here if bgp_large_community ~ [(205369,2,101)] then return false; # from peer, reject advertisement if bgp_large_community ~ [(205369,2,102)] then return false; # from upstream, reject advertisement return true; } Similarly, a similar check mechanism applies to DN42. Based on this mechanism, isolating the DN42 and IANA networks becomes very straightforward. The above is just a general explanation; finer community design and various export policies could be further elaborated, but that would turn into another article on routing policy details. This part actually draws heavily from たのしい’s BGP Communities; after all, both infrastructures need to interoperate, and many community specifications are basically copied. Thanks to たのしい for providing the ideas~ Route Lifecycle Phase 1: Ingress / Import When all external routes enter the system, they first pass through the import filter of the corresponding domain. IANA has three types of sources: upstream; peer / IX; downstream. DN42 also has two main types of sources: regular transit / eBGP peer; IX / route server. In addition, the local machine also originates some routes: underlay loopback; DN42 overlay address; IANA own / anycast address. These routes are tagged with Large Communities upon entering the confederation. All subsequent filters must be adjusted based on this. Phase 2: Core / Intra Confederation After entering the BIRD RIB, routes propagate between nodes via the internal Intra BGP Confederation. Here, Intra Confederation is not just about handling connectivity, but more importantly, allowing internal routes to carry policy information as well. The previous OSPF was great at solving: Where is this loopback? How to reach this next-hop? But it could not convey AS information and policy intents, for example: This route was learned from an IANA peer and must not be exported to another peer; This route is DN42 underlay-only and must never be leaked to eBGP; This route is a downstream customer prefix and can be exported to upstream. All of these are strengths of BGP policy. So moving the internal backbone from OSPF / full-mesh iBGP to confederation-style BGP is essentially upgrading the internal control plane to a "policy-carryable control plane". Phase 3: Egress / Export When a route is about to leave the infrastructure, it passes through the export filter again. Here, based on communities, systemic prevention of route leaks and macro-control like AS prepending can be performed. For example: DN42 underlay routes carry an underlay-only community and thus are not exported to DN42 eBGP; Routes learned from an IANA upstream are not exported to another upstream; Routes learned from an IANA peer are not exported to another peer; Routes learned from a DN42 IX are not exported back to IX; NO_EXPORT / NO_ADVERTISE are respected; Before external export, internal confederation ASes and private communities are removed. In summary, it can be illustrated by this diagram: flowchart LR classDef ext fill:#eef7ff,stroke:#5b8def,stroke-width:1px; classDef local fill:#f5f5f5,stroke:#888,stroke-width:1px; classDef filter fill:#fff3d6,stroke:#d19a00,stroke-width:1px; classDef core fill:#eaf8ea,stroke:#3c9b43,stroke-width:1px; classDef export fill:#fdecec,stroke:#d45a5a,stroke-width:1px; subgraph SRC["Route Source"] direction TB IU["IANA upstream<br/>transit / full routes"] IP["IANA peer / IX"] ID["IANA downstream<br/>customer prefixes"] DT["DN42 transit / eBGP peer"] DX["DN42 IX / route server"] LU["Local underlay loopback<br/>100.64.x.y / fd18:...:ffff"] LO["Local overlay address<br/>DN42 own / IANA own / anycast"] end subgraph INFRA["Bird2-Configuration Infrastructure"] direction LR subgraph IMPORT["Ingress / Import filters"] direction TB IUF["IANA upstream import<br/>tag: 205369:1:1<br/>tag: 205369:2:102"] IPF["IANA peer import<br/>tag: 205369:1:1<br/>tag: 205369:2:101"] IDF["IANA downstream import<br/>AS-SET / path check<br/>tag: 205369:2:100"] DTF["DN42 eBGP import<br/>prefix / ROA / self check<br/>tag: 4242422024:2:101/102"] DXF["DN42 IX import<br/>prefix / ROA check<br/>tag: 4242422024:2:101"] STF["Static origin<br/>underlay / overlay route tags"] end META["Large Community metadata layer<br/>domain + source + scope<br/>internal route API"] subgraph CORE["Internal control plane"] direction TB RIB["BIRD RIB"] CONFED["Intra BGP Confederation<br/>member AS: 422024xxxx<br/>policy: full / default / iana_full / no_iana"] WG["WireGuard underlay transport<br/>link-local next-hop<br/>fwmark policy routing"] UNDERLAY["net_underlay<br/>loopback + table 1142"] OVERLAY["net_overlay<br/>DN42 / IANA service addresses"] end subgraph EGRESS["Egress / Export filters"] direction TB DEXP["DN42 eBGP export<br/>reject underlay-only<br/>respect no-export/no-advertise"] DXEXP["DN42 IX export<br/>reject peer/transit learned routes"] IEXP["IANA upstream / peer export<br/>export local/downstream only<br/>reject peer/upstream learned"] DOWNEXP["IANA downstream export<br/>policy: default / own_only / reject"] KEXP["Kernel export<br/>krt_prefsrc by community"] CLEAN["cleanup before external export<br/>remove confed AS<br/>remove private communities"] end end subgraph DST["Route Destination"] direction TB OD["DN42 peers / transit"] OX["DN42 IX"] OI["IANA upstream / peer"] OC["IANA downstream"] KF["Linux kernel FIB<br/>actual packet forwarding"] end IU --> IUF IP --> IPF ID --> IDF DT --> DTF DX --> DXF LU --> STF LO --> STF IUF --> META IPF --> META IDF --> META DTF --> META DXF --> META STF --> META META --> RIB RIB <--> CONFED CONFED --- WG WG --- UNDERLAY RIB --- OVERLAY RIB --> DEXP RIB --> DXEXP RIB --> IEXP RIB --> DOWNEXP RIB --> KEXP DEXP --> CLEAN --> OD DXEXP --> CLEAN --> OX IEXP --> CLEAN --> OI DOWNEXP --> CLEAN --> OC KEXP --> KF class IU,IP,ID,DT,DX ext; class LU,LO local; class IUF,IPF,IDF,DTF,DXF,STF filter; class META,RIB,CONFED,WG,UNDERLAY,OVERLAY core; class DEXP,DXEXP,IEXP,DOWNEXP,KEXP,CLEAN export; class OD,OX,OI,OC,KF ext; Automation: Compiling Network Intent into Configuration All the above discussion on IPAM, Confederation AS, Underlay/Overlay, and BGP Communities would not truly solve the problem if it still ended up being maintained by hand-written configurations; it would just move the complexity from one form to another. Before this refactoring, adding a new node or peer often required simultaneous changes in many places: WireGuard configuration: interface name, listen port, peer public key, endpoint, allowed IPs BIRD configuration: BGP protocol name, neighbor address, neighbor interface, ASN, import/export filter IPAM-related configuration: loopback, link-local, router id, overlay address Deployment-related configuration: which nodes need rendering, which services need restarting Some special cases: nodes behind NAT, passive peers, special local AS, special filters Logically, these things describe the same thing: a connection belonging to a certain routing domain exists between two nodes. But in manual configuration, they end up scattered across WireGuard, BIRD, network interfaces, systemd/openrc, and other places. If just one field is missed or inconsistent, very strange problems can occur, such as WireGuard being up but BIRD neighbor pointing to the wrong interface; or the link-local being changed, but the peer session still having the old address. So in this refactoring, I didn't want Ansible to just be a tool that "fills YAML into Jinja2 templates". If we simply moved the original hand-written config into templates, we would essentially still be maintaining a pile of low-level configurations, just in a different file format. Since automation was introduced, the input should not be "I want to generate a configuration file that looks like this", but rather: Who is this node? Which tier / region does it belong to? Who does it interconnect with? Does this connection belong to DN42, IANA or Intra? What import/export policy should this connection use? Does it have special constraints like NAT, passive, endpoint override? As for the specific WireGuard interface wording, how the BIRD session is written, what the link-local address is – all these should, as much as possible, be generated by the automation system based on rules. Source of Truth: Node State, Not Config Files The input for this automation is mainly placed in Ansible's host_vars. The variables for each node are no longer just parameters needed for template rendering, but the description of the node's identity in the network. For example, a node will have at least: tier: node tier, e.g., backbone, IX, normal node region: region where the node is located node_id: node number within the same region node_token: a stable 16-bit token used to generate the IPv6 address suffix These fields are used for IPAM address management and identification information within the Confederation (as mentioned earlier, AS calculation and IP address derivation). The advantage of this approach is that when a new node joins, we just need to configure its basic metadata, and all its fundamental identities within the underlay are already determined. This significantly reduces subsequent maintenance effort. If link-local addresses were handwritten in every BIRD session, then any change to a node's token or address rule would require changes on all peers; but if the BIRD neighbor address is automatically derived from the peer node's metadata, it always stays consistent with the source of truth. Domain Intent: Declarative Peer Configuration The most important abstraction in this automation is Domain Intent, which treats the Session and its corresponding WireGuard tunnel binding as a single entity, then partitions them by the network type (DN42/IANA/Intra). Taking an Intra peer as an example, an internal connection fundamentally requires two types of configuration simultaneously: WireGuard interface, to provide the transport tunnel BIRD BGP session, to exchange routes over this tunnel If maintained manually, these two parts easily duplicate the same information: interface name peer node listen port endpoint neighbor interface BGP protocol name import/export policy So we abstract a regular Intra peer into a single peer intent. Simplified, it looks something like this: - node: tyo03-jp interface: intra_tyo03 wireguard: listen_port: 10234 passive: true bgp: protocol: intra_ibgp_tyo03 ipv4: import_policy: full export_policy: full ipv6: import_policy: full export_policy: full This declaration does not directly correspond to a single configuration file, but describes the peer intent: There is an internal connection named intra_tyo03 between the current node and tyo03-jp. This connection uses WireGuard for transport, and establishes an Intra BGP session over it, with BGP policy using full. Afterwards, Ansible automatically renders the corresponding WireGuard and BIRD configuration based on this. Thus, a peer relationship only needs to be described once, and the subsequent low-level configuration is generated automatically. For example, the BIRD neighbor's link-local address does not need to be handwritten; it can be automatically obtained from the peer node's tier / region / node_token. The WireGuard endpoint can also be automatically derived from the peer's ansible_host in the inventory and the peer's return peer listen_port. The biggest advantage of this is avoiding state inconsistency between multiple configuration layers. Automatic Endpoint Derivation and Passive Peers In my internal network architecture, not all nodes have perfectly symmetric public reachability. For example, some nodes / DN42 peers are behind NAT and can only initiate connections; some tunnels require a fixed endpoint as the listener, with the other end initiating. Therefore, the peer intent must support special cases like passive listening. If a peer is not passive, the automation can attempt to read ansible_host from the peer's inventory, combine it with the listen_port declared by the peer, and automatically generate the WireGuard endpoint: endpoint = peer_ansible_host + ":" + peer_return_listen_port If the peer is marked as passive, no endpoint is rendered, allowing the peer to actively initiate the connection. Essentially, the endpoint is determined by "how the peer is accessed" and "which port the peer listens on". These two pieces of information already exist in the inventory and peer intent, so there's no need to duplicate them. Of course, for extremely unusual links, the most basic manual override for the endpoint is retained. The goal of automation is not to eliminate all edge cases, but to make the normal cases require no special handling. Generic Scenarios & Exceptions This is essentially a fallback mechanism for special cases: Legacy tunnels Special internal peers carried over DN42 Nodes behind NAT Temporary workarounds Peers requiring strange BGP parameters Certain sessions needing special local AS Some routes only intended for the IANA domain, not the DN42 domain The final design kept: most regular Intra peers can generate WireGuard + BIRD via domain intent. DN42 peer WireGuard can also be generated from DN42 peer intents. However, some special BIRD sessions, such as those requiring separate local_as or special export policies, can still be explicitly declared using low-level configuration. Before final rendering, Ansible merges the generated configuration and legacy/manual overrides into the final configuration. The benefit is that automation covers 90% of the repetitive and error-prone parts, while still leaving room for the remaining 10% of special cases. Configuration Rendering The entire rendering process can be roughly understood as: flowchart TD A[host_vars: node metadata + domain intent] --> B[materialize domain intent] B --> C[effective_wireguard_interfaces] B --> D[effective_intra_ibgp_sessions] C --> E[render wg-quick configs] D --> F[render BIRD intra sessions] A --> G[render BIRD root / DN42 / IANA modules] A --> H[render underlay / overlay interfaces] E --> I[.rendered-wireguard] F --> J[.rendered] G --> J H --> K[.rendered-network] There are three types of final artifacts: .rendered/: BIRD configuration .rendered-wireguard/: WireGuard configuration .rendered-network/: underlay / overlay dummy interface configuration The BIRD configuration is further divided into several modules, written via Jinja2 templates; the actual overlay network separation is also handled here: root config: global defines, includes, kernel protocol DN42 module: DN42 filters, RPKI, eBGP peers, IX peers IANA module: upstream, downstream, peers, RPKI, static routes Intra module: confederation-style BGP sessions, static routes, internal filters Whether these modules are included is also decided automatically based on data as much as possible. For instance, if a node has no DN42 peers, there's no need to render and include ebgp.conf; no IX sessions, no need to include ix.conf. This reduces empty configurations and meaningless includes, and avoids some nodes failing template rendering due to missing related variables. Validation & Deployment The final deployment process also added some check and validation stages: render BIRD validate BIRD render WireGuard validate WireGuard deploy dummy interfaces deploy WireGuard deploy BIRD BIRD configuration is rendered and validated locally first to catch syntax errors as early as possible. WireGuard configuration is also rendered and validated first to avoid missing keys or generating obviously invalid wg-quick configs. Finally, after all validations pass, the underlying interfaces + WireGuard are deployed, and only then is BIRD uploaded and reloaded. Conclusion I'm exhausted; this article has drained me. Why is narrating this architecture in linear language so difficult? After sorting things out, most of the code was basically written by AI, and the results were decent. If you need a reference, see bird2-config/ansible at dev · iYoRoy-Network/bird2-config. The applicable scenarios for this solution are actually quite narrow; the DN42 + IANA BGP Player scene is really too niche xD. Moreover, a major reason this solution can function properly is that the IANA prefixes and DN42, along with the address ranges chosen for my own internal network, do not conflict at all. If there were address conflicts in the transit segments, one would probably still need to consider proper ISP isolation solutions like VRF/MPLS L3VPN. References: Configure BGP Confederation & Fake Confederation in Bird (Updated 2020-06-07) - Lan Tian @ Blog 浅谈 BGP 中的 Transit(中转)与 Peering(对等互联) | 网络蝙蝠侠部落
24/07/2026
58 Views
0 Comments
3 Stars
Building a Cross-Region K3s Cluster from Scratch - Calico No-Encapsulation CNI
# Preface I've actually wanted to play with a K8s cluster for a long time, but always felt that without sufficient knowledge, it would be too difficult to attempt. Recently, I spent some time studying DN42 and routing protocols like BGP and OSPF, and realized that it no longer feels so difficult. So I decisively started with K3s ( The main reason for choosing K3s over K8s is its lightweight nature: low resource requirements, no need to pull a bunch of images for deployment, availability of domestic mirrors… In short, K3s suits my needs better. I'm a beginner just starting to explore K3s, so please go easy on me if I make any mistakes~ # Analysis ## Choosing the CNI Component My current network architecture looks like this: ```mermaid graph TD subgraph ZeroTier Domestic subgraph WDS Gateway <--> VM1 Gateway <--> VM2 end NGB <--> Gateway HFE-NAS <--> Gateway NGB <--> HFE-NAS end subgraph IEPL Global-NIC <==OSPF==> CN-NIC end subgraph ZeroTier Global HKG02 <--> HKG04 TYO <--> HKG04 TYO <--> HKG02 end CN-NIC <--> NGB CN-NIC <--> HFE-NAS CN-NIC <--OSPF--> Gateway Global-NIC <--OSPF--> TYO Global-NIC <--OSPF--> HKG02 Global-NIC <--OSPF--> HKG04 %% Style definition: orange background, bold border to represent routers classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; class Global-NIC,CN-NIC,Gateway router; Among this, the WDS node is a Proxmox VE host with multiple VMs underneath. It advertises its VMs' IPv4 prefixes via OSPF. When Hong Kong nodes need to access a VM under the WDS node, they can do so by joining the OSPF internal network to achieve multi-hop reachability. This keeps the encapsulation layer count to only one, so there's no worry about MTU "disappearing act". I plan to create two new VMs under WDS to serve as the master and a node (temporarily called KubeMaster and KubeNode-WDS1). Then HKG04 (temporarily called KubeNode-HKG04) will also join the K3s cluster as a node. The simplest approach would be to use K3s's default Flannel as the CNI. However, Flannel is based on VXLAN, and adding another layer of my existing internal network would lead to the following MTU "disappearing act": Data packet -> Flannel VXLAN encapsulation -> ZeroTier encapsulation -> Physical link The actual usable MTU for inter-container communication would likely be compressed to 1350 or even lower. Therefore, I tried to find a CNI solution that can work directly on top of this internal network, and then I found Calico. As I understand, Calico uses BGP as its underlying routing protocol, supports starting in no-encapsulation (No-Encap) mode, and hands packets directly to the upper routers for routing. Thus, I chose Calico as the CNI component. Routing Design To ensure that intermediate routers know how to route Pod IPs, KubeMaster and KubeNode-WDS1 are under the Proxmox VE host. They need to establish BGP with HKG04 across the entire internal network. This means that every router at each intermediate level must learn the full BGP routes, so that the following routing path can be established: graph LR subgraph WDS KubeMaster KubeNode-WDS1 Gateway end subgraph IEPL CN-Namespace Global-Namespace end KubeNode-WDS1 <--> Gateway KubeMaster <--> Gateway <--> CN-Namespace <--> Global-Namespace <--> HKG04 %% Style definition: highlight nodes with routing capability classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; class Gateway,CN-Namespace,Global-Namespace router; Otherwise, any intermediate hop would drop packets because it doesn't recognize the source/destination IP. Also, due to the property of iBGP that routes learned from a neighbor cannot be propagated to the next iBGP neighbor, all BGP sessions between Gateway, CN-Namespace, Global-Namespace and the nodes need to enable Route Reflector; otherwise, nodes cannot correctly learn routes from each other. That said, this architecture would be more suitable for BGP Confederation, but my existing network is already quite complex, and adding BGP confederations would make later maintenance more troublesome. Moreover, my number of nodes is small, so the overhead of iBGP Full Mesh is acceptable. It's definitely not because I'm lazy (so Thus, the final network routing structure is as follows: graph TD subgraph WDS VM1 VM2 Gateway end subgraph IEPL CN-Namespace Global-Namespace end VM1 <-.Calico iBGP Full Mesh.-> VM2 VM1 <--iBGP Route Reflector--> Gateway VM2 <--iBGP Route Reflector--> Gateway <--iBGP--> CN-Namespace <--iBGP--> Global-Namespace <--iBGP Route Reflector--> HKG04 Gateway <--iBGP--> Global-Namespace HKG04 <-.Calico iBGP Full Mesh.-> VM1 VM2 <-.Calico iBGP Full Mesh.-> HKG04 %% Style definition classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; %% Mark nodes with routing/forwarding or RR functions as Router class Gateway,CN-Namespace,Global-Namespace router; The dashed-line BGP sessions are automatically created by Calico, while the solid-line parts need to be manually created by us. Keeping Calico's own iBGP Full Mesh is for future scalability, so that nodes can preferentially establish direct P2P connections via ZeroTier instead of taking a detour through the Route Reflector aggregation router. Deployment After clarifying the structure, deployment becomes simple. Enable Kernel Forwarding and Disable rp_filter Standard practice. echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf echo "net.ipv6.conf.default.forwarding=1" >> /etc/sysctl.conf echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf echo "net.ipv4.conf.default.rp_filter=0" >> /etc/sysctl.conf echo "net.ipv4.conf.all.rp_filter=0" >> /etc/sysctl.conf sysctl -p Install K3s Master Because the KubeMaster control plane node is located inside China, it's best to configure image acceleration: mkdir -p /etc/rancher/k3s cat <<EOF > /etc/rancher/k3s/registries.yaml mirrors: docker.io: endpoint: - "https://docker.m.daocloud.io" quay.io: endpoint: - "https://quay.m.daocloud.io" EOF Install using the mirror: curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \ INSTALL_K3S_MIRROR=cn INSTALL_K3S_EXEC=" \ --flannel-backend=none \ --disable-network-policy \ --cluster-cidr=10.42.0.0/16" sh - Note the need to specify --flannel-backend=none and --disable-network-policy to disable the default CNI component. Use cat /var/lib/rancher/k3s/server/node-token to view the token and record it. Worker Nodes For nodes inside China, configure image acceleration: mkdir -p /etc/rancher/k3s cat <<EOF > /etc/rancher/k3s/registries.yaml mirrors: docker.io: endpoint: - "https://docker.m.daocloud.io" quay.io: endpoint: - "https://quay.m.daocloud.io" EOF Then install K3s using the mirror and join the cluster: export INSTALL_K3S_MIRROR=cn export K3S_URL=https://<master node IP>:6443 # Replace with your master node's actual IP export K3S_TOKEN=K10...your token...::server:xxx # Replace with the full token obtained in the first step curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | sh - At this point, the status of each node should be NotReady because the CNI component is missing. Install Calico and Configure No-Encap Mode On the master, manually download https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml and install the Calico operator: kubectl create -f tigera-operator.yaml Configure a custom resource by creating a custom-resource.yaml file: apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: # Add image registry configuration registry: quay.m.daocloud.io calicoNetwork: ipPools: - blockSize: 26 cidr: 10.42.0.0/16 encapsulation: None natOutgoing: Enabled nodeSelector: all() Here, specify encapsulation: None to enable No-Encap mode. You can also modify the IPv4 CIDR here if needed. Then: kubectl apply -f custom-resource.yaml to perform the installation. Use: kubectl get pods -A -o wide to check Pod status, waiting for each node to finish pulling images. Configure BGP Topology Label Nodes Label nodes to specify that nodes under WDS connect to the Gateway's BGP in the WDS node, and nodes outside China connect to the BGP of the Global Namespace: kubectl label nodes kubemaster region=WDS kubectl label nodes kubenode-wds-1 region=WDS kubectl label nodes kubenode-hkg04 region=Global Calico Configuration Create a YAML configuration file: apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-domestic spec: nodeSelector: region == 'Domestic' # This part is not actually used; I originally designed a general aggregation router in the Domestic area peerIP: 100.64.0.108 asNumber: 64512 --- apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-wds spec: nodeSelector: region == 'WDS' peerIP: 192.168.100.1 asNumber: 64512 --- apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-global spec: nodeSelector: region == 'Global' peerIP: 100.64.1.106 asNumber: 64512 This means: All nodes with label region equal to Domestic will have a BGP session to 100.64.0.108 (the domestic aggregation router) using AS 64512 All nodes with label region equal to WDS will have a BGP session to 192.168.100.1 (the Gateway for all VMs under the WDS node) using AS 64512 All nodes with label region equal to Global will have a BGP session to 100.64.1.106 (the overseas aggregation router) using AS 64512 This achieves what is shown in the diagram: all VMs under the WDS node, including the master and KubeNode-WDS1, connect to the Gateway aggregation router of the WDS node, and all nodes in overseas areas connect to the overseas aggregation router. Configure Aggregation Router iBGP This part is simply a matter of writing Bird configuration files (easy). Here are a few examples: k3s/ibgp.conf: function is_insider_as(){ if bgp_path.len > 0 && !(bgp_path ~ [= 64512 =]) then { return false; } if net ~ [ 10.42.0.0/16{16,32} ] then { return true; } return false; } template bgp k3sbackbone{ local as K3S_AS; router id INTRA_ROUTER_ID; neighbor as K3S_AS; ipv4{ table intra_table_v4; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; extended next hop; }; ipv6{ table intra_table_v6; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; }; template bgp k3speers{ local as K3S_AS; neighbor as K3S_AS; router id INTRA_ROUTER_ID; rr client; rr cluster id INTRA_ROUTER_ID; ipv4{ table intra_table_v4; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; ipv6{ table intra_table_v6; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; }; include "ibgpeers/*"; ibgpeers/backbone-cn.conf: protocol bgp 'k3s_backbone_cn_v4' from k3sbackbone{ neighbor fd18:3e15:61d0:cafe:f001::1; }; ibgpeers/master.conf: protocol bgp 'k3s_master_v4' from k3speers{ neighbor 192.168.100.251; }; Main points: it's best not to enable Route Reflector between the aggregation routers, and remember to enable next hop self. After everything is done, using kubectl get nodes should show all nodes as Ready: NAME STATUS ROLES AGE VERSION kubemaster Ready control-plane 2d23h v1.34.5+k3s1 kubenode-hkg04 Ready <none> 11h v1.34.6+k3s1 kubenode-wds-1 Ready <none> 2d7h v1.34.5+k3s1 Use kubectl get pods -A -o wide to view Pods: NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES calico-system calico-kube-controllers-64fc874957-6bdlz 1/1 Running 0 5h38m 10.42.253.136 kubenode-hkg04 <none> <none> calico-system calico-node-2qz82 1/1 Running 0 4h24m 10.2.5.7 kubenode-hkg04 <none> <none> calico-system calico-node-dhl2c 1/1 Running 0 4h24m 192.168.100.251 kubemaster <none> <none> calico-system calico-node-nbpkj 1/1 Running 0 4h23m 192.168.100.252 kubenode-wds-1 <none> <none> calico-system calico-typha-7bb5db4bdc-rfpwg 1/1 Running 0 5h38m 10.2.5.7 kubenode-hkg04 <none> <none> calico-system calico-typha-7bb5db4bdc-rwwr5 1/1 Running 0 5h38m 192.168.100.251 kubemaster <none> <none> calico-system csi-node-driver-jglwp 2/2 Running 0 5h38m 10.42.64.68 kubenode-wds-1 <none> <none> calico-system csi-node-driver-jqjsc 2/2 Running 0 5h38m 10.42.253.137 kubenode-hkg04 <none> <none> calico-system csi-node-driver-vk26s 2/2 Running 0 5h38m 10.42.141.16 kubemaster <none> <none> kube-system coredns-695cbbfcb9-8fx4p 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.14 kubemaster <none> <none> kube-system helm-install-traefik-crd-5bkwx 0/1 Completed 0 2d23h <none> kubemaster <none> <none> kube-system helm-install-traefik-m9fgj 0/1 Completed 1 2d23h <none> kubemaster <none> <none> kube-system local-path-provisioner-546dfc6456-dmn4g 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.15 kubemaster <none> <none> kube-system metrics-server-c8774f4f4-2wkwh 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.12 kubemaster <none> <none> kube-system svclb-traefik-999cddce-hpmcm 2/2 Running 6 (7h26m ago) 11h 10.42.253.134 kubenode-hkg04 <none> <none> kube-system svclb-traefik-999cddce-q4225 2/2 Running 2 (7h27m ago) 2d22h 10.42.141.9 kubemaster <none> <none> kube-system svclb-traefik-999cddce-xmd64 2/2 Running 2 (7h26m ago) 2d6h 10.42.64.66 kubenode-wds-1 <none> <none> kube-system traefik-788bc4688c-vbbhj 1/1 Running 1 (7h27m ago) 2d22h 10.42.141.13 kubemaster <none> <none> tigera-operator tigera-operator-6b95bbf4db-vl46l 1/1 Running 1 (7h27m ago) 2d23h 192.168.100.251 kubemaster <none> <none> Use kubectl exec -it -n calico-system <calico-node-xxxx> -- birdcl s p to check the status of Bird: root@KubeMaster:~/kube/calico# kubectl exec -it -n calico-system calico-node-2qz82 -- birdcl s p Defaulted container "calico-node" out of: calico-node, flexvol-driver (init), install-cni (init) BIRD v0.3.3+birdv1.6.8 ready. name proto table state since info static1 Static master up 08:58:17 kernel1 Kernel master up 08:58:17 device1 Device master up 08:58:17 direct1 Direct master up 08:58:17 Mesh_192_168_100_251 BGP master up 08:58:33 Established Mesh_192_168_100_252 BGP master up 08:59:00 Established Node_100_64_1_106 BGP master up 12:57:44 Established ip r shows the system routing table: root@KubeMaster:~/kube/calico# ip r default via 192.168.100.1 dev eth0 proto static 10.42.64.64/26 proto bird nexthop via 192.168.100.1 dev eth0 weight 1 nexthop via 192.168.100.252 dev eth0 weight 1 blackhole 10.42.141.0/26 proto bird 10.42.141.9 dev caliac6501d3794 scope link 10.42.141.12 dev calib07c23291bb scope link 10.42.141.13 dev caliab16e60bd19 scope link 10.42.141.14 dev calid5959219080 scope link 10.42.141.15 dev cali026d8f1ddb7 scope link 10.42.141.16 dev califa657ba417a scope link 10.42.253.128/26 via 192.168.100.1 dev eth0 proto bird 192.168.100.0/24 dev eth0 proto kernel scope link src 192.168.100.251 Ping a Pod's IP – if everything is fine, it should work directly: root@KubeMaster:~/kube/calico# ping 10.42.253.137 PING 10.42.253.137 (10.42.253.137) 56(84) bytes of data. 64 bytes from 10.42.253.137: icmp_seq=1 ttl=60 time=33.7 ms 64 bytes from 10.42.253.137: icmp_seq=2 ttl=60 time=33.5 ms ^C --- 10.42.253.137 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1002ms rtt min/avg/max/mdev = 33.546/33.632/33.718/0.086 ms Tune MTU This step is actually for stability…? Tests have shown that although my ZeroTier MTU is 1420, packets start to fragment around 1392 bytes (test with ping -M do -s <packet size> <Pod_IP>). Therefore, force the Pod MTU to 1370: root@KubeMaster:~/kube/calico# cat patch-mtu.yaml apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: calicoNetwork: mtu: 1370 nodeAddressAutodetectionV4: firstFound: true root@KubeMaster:~/kube/calico# kubectl apply -f patch-mtu.yaml installation.operator.tigera.io/default configured
05/04/2026
146 Views
0 Comments
6 Stars
DN42&OneManISP - Troubleshooting OSPF Source Address in a Coexistence Environment
Backstory As mentioned in the previous post of this series, because the VRF solution was too isolating, the DNS service I deployed on the HKG node (172.20.234.225) became inaccessible from the DN42 network. Research indicated this could be achieved by setting up veth or NAT forwarding, but due to the scarcity of available documentation, I ultimately abandoned the VRF approach. Structure Analysis This time, I planned to place both DN42 and clearnet BGP routes into the system's main routing table, then separate them for export using filters to distinguish which should be exported. For clarity, I stored the configuration for the DN42 part and the clearnet part (hereinafter referred to as inet) separately, and then included them from the main configuration file. Also, since there should ideally only be one kernel configuration per routing table, I merged the DN42 and inet kernel parts, keeping only one instance. After multiple optimizations and revisions, my final directory structure is as follows: /etc/bird/ ├─envvars ├─bird.conf: Main Bird config file, defines basic info (ASN, IP, etc.), includes sub-configs below ├─kernel.conf: Kernel config, imports routes into the system routing table ├─dn42 | ├─defs.conf: DN42 function definitions, e.g., is_self_dn42_net() | ├─ibgp.conf: DN42 iBGP template | ├─rpki.conf: DN42 RPKI route validation | ├─ospf.conf: DN42 OSPF internal network | ├─static.conf: DN42 static routes | ├─ebgp.conf: DN42 Peer template | ├─ibgp | | └<ibgp configs>: DN42 iBGP configs for each node | ├─ospf | | └backbone.conf: OSPF area | ├─peers | | └<ibgp configs>: DN42 Peer configs for each node ├─inet | ├─peer.conf: Clearnet Peer | ├─ixp.conf: Clearnet IXP connection | ├─defs.conf: Clearnet function definitions, e.g., is_self_inet_v6() | ├─upstream.conf: Clearnet upstream | └static.conf: Clearnet static routes I separated the function definitions because I needed to reference them in the filters within kernel.conf, so I isolated them for early inclusion. After filling in the respective configurations and setting up the include relationships, I ran birdc configure and it started successfully. So, case closed... right? Problems occurred After running for a while, I suddenly found that I couldn't ping the HKG node from my internal devices, nor could I ping my other internal nodes from the HKG node. Strangely, external ASes could ping my other nodes or other external ASes through my HKG node, and my internal nodes could also ping other non-directly connected nodes (e.g., 226(NKG)->225(HKG)->229(LAX)) via the HKG node. Using ip route get <other internal node address> revealed: root@iYoRoyNetworkHKG:/etc/bird# ip route get 172.20.234.226 172.20.234.226 via 172.20.234.226 dev dn42_nkg src 23.149.120.51 uid 0 cache See the problem? The src address should have been the HKG node's own DN42 address (configured on the OSPF stub interface), but here it showed the HKG node's clearnet address instead. Attempting to read the route learned by Bird using birdc s r for 172.20.234.226: root@iYoRoyNetworkHKGBGP:/etc/bird/dn42/ospf# birdc s r for 172.20.234.226 BIRD 2.17.1 ready. Table master4: 172.20.234.226/32 unicast [dn42_ospf_iyoroynet_v4 00:30:29.307] * I (150/50) [172.20.234.226] via 172.20.234.226 on dn42_nkg onlink Looks seemingly normal...? Theoretically, although the DN42 source IP is different from the usual, DN42 rewrites krt_prefsrc when exporting to the kernel to inform the kernel of the correct source address, so this issue shouldn't occur: protocol kernel kernel_v4{ ipv4 { import none; export filter { if source = RTS_STATIC then reject; + if is_valid_dn42_network() then krt_prefsrc = DN42_OWNIP; accept; }; }; } protocol kernel kernel_v6 { ipv6 { import none; export filter { if source = RTS_STATIC then reject; + if is_valid_dn42_network_v6() then krt_prefsrc = DN42_OWNIPv6; accept; }; }; } Regarding krt_prefsrc, it stands for Kernel Route Preferred Source. This attribute doesn't manipulate the route directly but instead attaches a piece of metadata to it. This metadata directly instructs the Linux kernel to prioritize the specified IP address as the source address for packets sent via this route. I was stuck on this for a long time. The Solution Finally, during an unintentional attempt, I added the krt_prefsrc rewrite to the OSPF import configuration as well: protocol ospf v3 dn42_ospf_iyoroynet_v4 { router id DN42_OWNIP; ipv4 { - import where is_self_dn42_net() && source != RTS_BGP; + import filter { + if is_self_dn42_net() && source != RTS_BGP then { + krt_prefsrc=DN42_OWNIP; + accept; + } + reject; + }; export where is_self_dn42_net() && source != RTS_BGP; }; include "ospf/*"; }; protocol ospf v3 dn42_ospf_iyoroynet_v6 { router id DN42_OWNIP; ipv6 { - import where is_self_dn42_net_v6() && source != RTS_BGP; + import filter { + if is_self_dn42_net_v6() && source != RTS_BGP then { + krt_prefsrc=DN42_OWNIPv6; + accept; + } + reject; + }; export where is_self_dn42_net_v6() && source != RTS_BGP; }; include "ospf/*"; }; After running this, the src address became correct, and mutual pinging worked. Configuration files for reference: KaguraiYoRoy/Bird2-Configuration
29/10/2025
170 Views
0 Comments
1 Stars
DN42&OneManISP - Using VRF to Run Clearnet BGP and DN42 on the Same Machine
Background Currently, clearnet BGP and DN42 each use a separate VPS in the same region, meaning two machines are required per region. After learning about VRF from a group member, I explored using VRF to enable a single machine to handle both clearnet BGP and DN42 simultaneously. Note: Due to its isolation nature, the VRF solution will prevent DN42 from accessing services on the host. If you need to run services (like DNS) on the server for DN42, you might need additional port forwarding or veth configuration, which is beyond the scope of this article. (This is also the reason why I ultimately did not adopt VRF in my production environment). Advantages of VRF Although DN42 uses private IP ranges and internal ASNs, which theoretically shouldn't interfere with clearnet BGP, sharing the same routing table can lead to issues like route pollution and management complexity. VRF (Virtual Routing and Forwarding) allows creating multiple routing tables on a single machine. This means we can isolate DN42 routes into a separate routing table, keeping them apart from the clearnet routing table. The advantages include: Absolute Security and Policy Isolation: The DN42 routing table is isolated from the clearnet routing table, fundamentally preventing route leaks. Clear Operation and Management: Use commands like birdc show route table t_dn42 and birdc show route table t_inet to view and debug two completely independent routing tables, making things clear at a glance. Fault Domain Isolation: If a DN42 peer flaps, the impact is confined to the dn42 routing table. It won't consume routing computation resources for the clearnet instance nor affect clearnet forwarding performance. Alignment with Modern Network Design Principles: Using VRF for different routing domains (production, testing, customer, partner) is standard practice in modern network engineering. It logically divides your device into multiple virtual routers. Configuration System Part Creating the VRF Interface Use the following commands to create a VRF device named dn42-vrf and associate it with the system's routing table number 1042: ip link add dn42-vrf type vrf table 1042 ip link set dev dn42-vrf up # Enable it You can change the routing table number according to your preference, but avoid the following reserved routing table IDs: Name ID Description unspec 0 Unspecified, rarely used main 254 Main routing table, where most ordinary routes reside default 253 Generally unused, reserved local 255 Local routing table, contains 127.0.0.1/8, local IPs, broadcast addresses, etc. Cannot be modified Associating Existing Network Interfaces with VRF In my current DN42 setup, several WireGuard interfaces and a dummy interface are used for DN42. Therefore, associate these interfaces with the VRF: ip link set dev <interface_name> master dn42-vrf Note: After associating an interface with a VRF, it might lose its IP addresses. Therefore, you need to readd the addresses, for example: ip addr add 172.20.234.225 dev dn42 After completion, ip a should show the corresponding interface's master as dn42-vrf: 156: dn42: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue master dn42-vrf state UNKNOWN group default qlen 1000 link/ether b6:f5:28:ed:23:04 brd ff:ff:ff:ff:ff:ff inet 172.20.234.225/32 scope global dn42 valid_lft forever preferred_lft forever inet6 fd18:3e15:61d0::1/128 scope global valid_lft forever preferred_lft forever inet6 fe80::b4f5:28ff:feed:2304/64 scope link valid_lft forever preferred_lft forever Persistence I use ifupdown2 to automatically load the dummy interface and VRF device on boot. auto dn42-vrf iface dn42-vrf inet manual vrf-table 1042 auto dn42 iface dn42 inet static pre-up ip link add $IFACE type dummy || true vrf dn42-vrf address <IPv4 Address>/32 address <IPv6 Address>/128 post-down ip link del $IFACE My dummy interface is named dn42; modify accordingly if yours is different. After creation, use ifup dn42-vrf && ifup dn42 to start the dummy interface. Note: The number prefix for the VRF device file should be smaller than that of the dummy interface file, ensuring the VRF device starts first. WireGuard Tunnels Add PostUp commands to associate them with the VRF and readd their addresses. Example: [Interface] PrivateKey = [Data Redacted] ListenPort = [Data Redacted] Table = off Address = fe80::2024/64 + PostUp = ip link set dev %i master dn42-vrf + PostUp = ip addr add fe80::2024/64 dev %i PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0 [Peer] PublicKey = [Data Redacted] Endpoint = [Data Redacted] AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8 Then restart the tunnel. Bird2 Part First, define two routing tables for DN42's IPv4 and IPv6: ipv4 table dn42_table_v4; ipv6 table dn42_table_v6 Then, specify the VRF and system routing table number in the kernel protocol, and specify the previously created v4/v6 routing tables in the IPv4/IPv6 sections: protocol kernel dn42_kernel_v6{ + vrf "dn42-vrf"; + kernel table 1042; scan time 20; ipv6 { + table dn42_table_v6; import none; export filter { if source = RTS_STATIC then reject; krt_prefsrc = DN42_OWNIPv6; accept; }; }; }; protocol kernel dn42_kernel_v4{ + vrf "dn42-vrf"; + kernel table 1042; scan time 20; ipv4 { + table dn42_table_v4; import none; export filter { if source = RTS_STATIC then reject; krt_prefsrc = DN42_OWNIP; accept; }; }; } For protocols other than kernel, add the VRF and the independent IPv4/IPv6 tables, but do not specify the system routing table number: protocol static dn42_static_v4{ + vrf "dn42-vrf"; route DN42_OWNNET reject; ipv4 { + table dn42_table_v4; import all; export none; }; } protocol static dn42_static_v6{ + vrf "dn42-vrf"; route DN42_OWNNETv6 reject; ipv6 { + table dn42_table_v6; import all; export none; }; } In summary: Configure a VRF and the previously defined routing tables for everything related to DN42. Only the kernel protocol needs the system routing table number specified; others do not. Apply the same method to BGP, OSPF, etc. However, I chose to use separate Router IDs for the clearnet and DN42, so a separate Router ID needs to be configured: # /etc/bird/dn42/ospf.conf protocol ospf v3 dn42_ospf_iyoroynet_v4 { + vrf "dn42-vrf"; + router id DN42_OWNIP; ipv4 { + table dn42_table_v4; import where is_self_dn42_net() && source != RTS_BGP; export where is_self_dn42_net() && source != RTS_BGP; }; include "ospf/*"; }; protocol ospf v3 dn42_ospf_iyoroynet_v6 { + vrf "dn42-vrf"; + router id DN42_OWNIP; ipv6 { + table dn42_table_v6; import where is_self_dn42_net_v6() && source != RTS_BGP; export where is_self_dn42_net_v6() && source != RTS_BGP; }; include "ospf/*"; }; # /etc/bird/dn42/ebgp.conf ... template bgp dnpeers { + vrf "dn42-vrf"; + router id DN42_OWNIP; local as DN42_OWNAS; path metric 1; ipv4 { + table dn42_table_v4; ... }; ipv6 { + table dn42_table_v6; ... }; } include "peers/*"; After completion, reload the configuration with birdc c. Now, we can view the DN42 routing table separately using ip route show vrf dn42-vrf: root@iYoRoyNetworkHKGBGP:~# ip route show vrf dn42-vrf 10.26.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 10.29.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 10.37.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 ... You can also ping through the VRF using the -I dn42-vrf parameter: root@iYoRoyNetworkHKGBGP:~# ping 172.20.0.53 -I dn42-vrf ping: Warning: source address might be selected on device other than: dn42-vrf PING 172.20.0.53 (172.20.0.53) from 172.20.234.225 dn42-vrf: 56(84) bytes of data. 64 bytes from 172.20.0.53: icmp_seq=1 ttl=64 time=3.18 ms 64 bytes from 172.20.0.53: icmp_seq=2 ttl=64 time=3.57 ms 64 bytes from 172.20.0.53: icmp_seq=3 ttl=64 time=3.74 ms 64 bytes from 172.20.0.53: icmp_seq=4 ttl=64 time=2.86 ms ^C --- 172.20.0.53 ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 3006ms rtt min/avg/max/mdev = 2.863/3.337/3.740/0.341 ms Important Notes If the VRF device is reloaded, all devices originally associated with the VRF need to be reloaded as well, otherwise they won't function correctly. Currently, DN42 cannot access services inside the host configured with VRF. A future article might explain how to allow traffic within the VRF to access host services (Adding to the TODO list). I learned from a friend that by setting net.ipv4.tcp_l3mdev_accept=1 and net.ipv4.udp_l3mdev_accept=1, it is possible to allow the listening sockets in the global space to accept connection requests from the VRF domain, thus achieving cross-vrf listening services. Reference Articles:: Run your MPLS network with BIRD
16/09/2025
255 Views
0 Comments
1 Stars
OneManISP - Ep.2 Announcing Our Own IP Prefix to the World
Preface In the previous article, we successfully registered an ASN and obtained an IPv6 address block. Now, we will announce this block to the world. Setting Up the Subnet Object in the RIPE Database It's important to note that the minimum IPv6 prefix allowed for announcement on the public internet is /48. This means if you only have a single /48 block, you cannot break it down into smaller segments. Therefore, I later leased a separate /40 block, intending to split it into multiple /48s for announcement. The IPv6 block I obtained is 2a14:7583:f200::/40, and I plan to split out 2a14:7583:f203::/48 for use with Vultr. If you don't need to split your block, please skip directly to the "Creating the Route Object" section. Splitting the Prefix First, go to Create "inet6num" object - RIPE Database and fill in the following: inet6num: The IP block you want to split out, in CIDR format. netname: Network name. country: The country to which the IP block belongs, must conform to the ISO 3166 standard (can be selected directly in the RIPE DB). admin-c: The primary key value of the Role object created earlier. tech-c: The primary key value of the Role object created earlier. status: Keep ASSIGNED This step splits a smaller /48 address block from your obtained allocation. Creating the Route Object Go to Create "route6" object - RIPE Database and fill in the following: route6: The IPv6 address block you intend to announce, in CIDR format. origin: The ASN you applied for, including the 'AS' prefix. This step declares that your ASN is permitted to use this address block for originating BGP routes. Applying for BGP Session with a VPS Provider This time I'm using a machine from Vultr. Their BGP Session setup is very beginner-friendly, with their own validation system. Furthermore, their upstream has good filters ensuring that incorrect route advertisements generally won't affect the public internet. (I forgot to take screenshots during my configuration, but you can refer to the section 申请 Vultr 的 BGP 广播功能 in Bao Shuo's article 年轻人的第一个 ASN for reference.) Go to BGP - Vultr.com, select Get Started, and fill in your ASN and IPv6 block information as required. For the LOA (Letter Of Authorization), you can refer to this template: LOA-template.docx (I rewrote one for individuals as most templates found online are for companies). After submission, the system will automatically create a ticket, and you will see your ASN and IP block in a pending verification state: Click Start, and the system will send a verification email to the abuse-mailbox email address registered with your Role object: The received email looks like this: The top link represents approving the authorization for Vultr to announce your IP block, and the bottom one is for disapproval. Click the top link, which will take you to Vultr's webpage: Then click Approve Announcement. Both the ASN and the IP block need to be verified once. Next, wait for the Vultr staff to review and complete the process. Then, in your VPS control panel, you will see the BGP tab, where you can find the upstream information: I must commend Vultr's ticket efficiency here; it took me an average of only about 10 minutes from creating the ticket requesting authorization to completion. (In contrast, the average weekday ticket response time at iFog GmbH was around 1 day, which is much slower in comparison). The process with other VPS providers is generally similar. You need to inform their staff of the ASN and IP block you want to announce. After verifying ownership, the staff will configure the corresponding BGP Session for you. Advertisement! You should have received the following information from your upstream: Upstream's ASN Upstream's IP address for the BGP Session (Optional) Password The operating system I use is Debian 12 Bookworm, using Bird2 as the routing software. I updated Bird2 to the latest version following the section "Update Bird2 to v2.16 or above" in this article. The upstream ASN Vultr gave me is 64515, the upstream BGP Session address is 2001:19f0:ffff::1, and the VPS's BGP Session address is 2001:19f0:0006:0ff5:5400:05ff:fe96:881f. My Bird2 configuration file is modified from the configuration file used in DN42: log syslog all; define OWNAS = 205369; define OWNIPv6 = 2a14:7583:f203::1; define OWNNETv6 = 2a14:7583:f203::/48; define OWNNETSETv6 = [ 2a14:7583:f203::/48+ ]; router id 45.77.x.x; protocol device { scan time 10; } function is_self_net_v6() { return net ~ OWNNETSETv6; } protocol kernel { scan time 20; ipv6 { import none; export filter { if source = RTS_STATIC then reject; krt_prefsrc = OWNIPv6; accept; }; }; }; protocol static { route OWNNETv6 reject; ipv6 { import all; export none; }; } template bgp upstream { local as OWNAS; path metric 1; multihop; ipv6 { import filter { if net ~ [::/0] then reject; accept; }; export filter { if is_self_net_v6() then accept; reject; }; import limit 1000 action block; }; graceful restart; } protocol bgp 'Vultr_v6' from upstream{ local 2001:19f0:0006:0ff5:5400:05ff:fe96:881f as OWNAS; password "123456"; neighbor 2001:19f0:ffff::1 as 64515; } A few noteworthy points: The import rule in the upstream template here rejects the default route. This prevents the routing table sent by the upstream from overwriting local default gateway routes and other routing information. If we have multiple BGP neighbors, this could cause detours or even routing loops. The upstream template specifies multihop (multihop;) because Vultr's BGP peer is not directly reachable. Without setting multihop, the BGP session would get stuck in the Idle state. If your BGP upstream is directly connected, you can omit this line or set it to direct;. After filling in the configuration file, run birdc configure to load the configuration. Run birdc show protocols to check the status. If all goes well, you should see the BGP session state as Established: At this point, you can take a break and wait for global routing convergence. After about half an hour, open bgp.tools and query your /48 block. You should see that it has been successfully received by the global internet, and you can see our upstream information: Next, we create a dummy interface on the VPS and assign a single IPv6 address from the block allocated for this machine. For example, I assigned 2a14:7583:f203::1 to my machine: ip link add dummy0 type dummy ip addr add 2a14:7583:f203::1/128 dev dummy0 Then, using your own PC, you should be able to ping this address, and traceroute will show the complete routing path: Thanks to Mi Lu for the technical support! Reference Articles: 自己在家开运营商 Part.2 - 向世界宣告 IP 段 (BGP Session & BIRD) 年轻人的第一个 ASN - 宝硕博客 BGPlayer 从零开始速成指北 - 开通 Vultr 的 BGP 广播功能 - AceSheep BGP (2) 在 Vultr 和 HE 使用自己的 IPV6 地址 - 131's Blog
20/08/2025
321 Views
0 Comments
2 Stars
1
2