[Fun Experiment] A LAN Spanning 20km: Seamlessly Merging Remote Networks on OpenWrt Using ZeroTier + OSPF
Background I was originally setting up my own ZeroTier "big internal network". Because the network structure is relatively complex, I decided to use OSPF instead of static routes to configure internal routing. I had tried to configure ZeroTier on my home OpenWrt before but never succeeded. Recently, I took it out again to work on it and discovered it was a configuration issue with OpenWrt. After fixing it, I was chatting with a good friend and had an idea: Kagura iYoRoy: 02-10 14:49:05 Hey... Kagura iYoRoy: 02-10 14:49:06 Then... Kagura iYoRoy: 02-10 14:49:20 If you also set up OSPF on your router... Kagura iYoRoy: 02-10 14:49:27 Our two home networks would be directly interconnected, huh? ( Let's do it! Basic Information Local Side Router OS: OpenWrt, X-WRT 26.04_b202601250827 LAN IPv4 Prefix: 192.168.3.0/24 ISP: Hefei China Unicom NAT Environment: NAT1 Remote Side Router OS: OpenWrt, X-WRT 25.04_b202510240128 LAN IPv4 Prefix: 192.168.1.0/24 ISP: Hefei China Mobile NAT Environment: NAT1 Installing ZeroTier and Using a Self-Hosted Planet I used ZTNet as the self-hosted Controller. The setup process won't be elaborated here as you can find it online. The OpenWrt version I'm using has started using apk instead of opkg as the package manager. Use apk to install zerotier-one directly: apk add zerotier After completion, open /etc/config/zerotier to find the default configuration file. config zerotier 'global' # Sets whether ZeroTier is enabled or not option enabled 0 # Sets the ZeroTier listening port (default 9993; set to 0 for random) #option port '9993' # Client secret (leave blank to generate a secret on first run) option secret '' # Path of the optional file local.conf (see documentation at # https://docs.zerotier.com/config#local-configuration-options) #option local_conf_path '/etc/zerotier.conf' # Persistent configuration directory (to perform other configurations such # as controller mode or moons, etc.) #option config_path '/etc/zerotier' # Copy the contents of the persistent configuration directory to memory # instead of linking it, this avoids writing to flash #option copy_config_path '1' # Network configuration, you can have as many configurations as networks you # want to join (the network name is optional) config network 'earth' # Identifier of the network you wish to join option id '8056c2e21c000001' # Network configuration parameters (all are optional, if not indicated the # default values are set, see documentation at # https://docs.zerotier.com/config/#network-specific-configuration) option allow_managed '1' option allow_global '0' option allow_default '0' option allow_dns '0' # Example of a second network (unnamed as it is optional) #config network # option id '1234567890123456' # option allow_managed '1' # option allow_global '0' # option allow_default '0' # option allow_dns '0' Modify it according to your needs: config zerotier 'global' option enabled '1' # Enable ZeroTier client service option config_path '/etc/zerotier' # Persistent directory: for storing identity secret, Moon node definitions, and network settings option secret '' # Leave secret blank: identity will be auto-generated on first run and saved to identity.secret option copy_config_path '1' # Flash protection policy: copy config to memory on startup. If set to 0, read/write directly to Flash config network 'earth' option id '<network ID>' # 16-digit ZeroTier Network ID option allow_managed '1' # Allow receiving controller-assigned IPs, routes, and tags option allow_global '1' # Allow receiving globally routable IPv6 unicast addresses (GUA) via ZeroTier option allow_default '0' # Allow ZeroTier to take over the default gateway (similar to a global proxy) option allow_dns '1' # Allow receiving and using DNS servers configured in the ZeroTier control panel Regarding copy_config_path '1' Because the ZeroTier working directory /var/lib/zerotier-one is part of tmpfs in OpenWrt, its contents are cleared on reboot. Therefore, configurations like planet, identity, and network files need to be stored in the router's Flash storage, i.e., the path set in config_path. The default logic is to create a soft link from the configured config_path to /var/lib/zerotier-one on startup to achieve persistence. All read/write operations in /var/lib/zerotier-one are then written to Flash. However, frequent ZeroTier read/writes can significantly reduce Flash lifespan. Enabling copy_config_path '1' specifies that on ZeroTier startup, the configurations from config_path are copied directly into /var/lib/zerotier-one. This greatly extends the internal Flash lifespan, but the downside is that modifications made via zerotier-cli are not automatically synced back to Flash by default, making this option less suitable for scenarios requiring frequent configuration adjustments. After making changes, use: /etc/init.d/zerotier start /etc/init.d/zerotier enable to start ZeroTier and enable auto-start on boot. On first startup, if the secret field was left empty, it will be auto-generated. After startup, copy all files from /var/lib/zerotier-one to /etc/zerotier. Download the Planet file to the config_path set above, i.e., /etc/zerotier. After completion, restart ZeroTier: /etc/init.d/zerotier restart That's it. Then, go to your ZeroTier Controller console, and you should see the new device has joined. Next, you may need to allow ZeroTier traffic through the firewall. This step can be referenced from other online tutorials. I chose to allow all traffic; it shouldn't be a big issue under NAT1. Installing and Configuring Bird2 I didn't expect the Bird2 version in the apk repository to be very recent. As of this writing on 2026-02-10, the Bird2 version in apk is 2.18 Use the following command to install: apk add bird2 # bird daemon itself apk add bird2c # birdc command Because OpenWrt's default bird configuration file is located at /etc/bird.conf, and I prefer modular referencing by placing different configurations in separate folders based on function, I chose to move the default config file to /etc/bird/bird.conf and store various config files within that folder. Open /etc/init.d/bird: #!/bin/sh /etc/rc.common # Copyright (C) 2010-2017 OpenWrt.org USE_PROCD=1 START=70 STOP=10 BIRD_BIN="/usr/sbin/bird" BIRD_CONF="/etc/bird.conf" BIRD_PID_FILE="/var/run/bird.pid" start_service() { mkdir -p /var/run procd_open_instance procd_set_param command $BIRD_BIN -f -c $BIRD_CONF -P $BIRD_PID_FILE procd_set_param file "$BIRD_CONF" procd_set_param stdout 1 procd_set_param stderr 1 procd_set_param respawn procd_close_instance } reload_service() { procd_send_signal bird } Change the BIRD_CONF value to /etc/bird/bird.conf: - BIRD_CONF="/etc/bird.conf" + BIRD_CONF="/etc/bird/bird.conf" Then create the /etc/bird folder. All subsequent OSPF configuration files will be placed here. Configuring OSPF My configuration file structure follows these rules: /etc/bird/bird.conf serves as the sole entry point, defining basic configurations like Router ID, filter prefixes, and then including other sub-configurations. Configurations for different networks are placed in separate folders, e.g., public internet parts in /etc/bird/inet/, DN42 parts in /etc/bird/dn42/, and my own internal network parts in /etc/bird/intra/. Each network has a defs.conf handling common functions (similar to utils in Golang development?). Thus, the final configuration file structure is: /etc/bird/bird.conf: Configuration entry point define INTRA_ROUTER_ID = 100.64.0.100; define INTRA_PREFIX_V4 = [ 100.64.0.0/16+, 192.168.0.0/16+ ]; # IPv4 prefixes allowed to be advertised via OSPF define INTRA_PREFIX_V6 = [ fd18:3e15:61d0::/48+ ]; # IPv6 prefixes allowed to be advertised via OSPF protocol device { scan time 10; }; ipv4 table intra_table_v4; # Define internal routing IPv4 table ipv6 table intra_table_v6; # Define internal routing IPv6 table include "intra/defs.conf"; include "intra/kernel.conf"; include "intra/ospf.conf"; The RouterID here is directly taken from the node's IPv4 address within the ZeroTier internal network. Separate tables are used for future safety, e.g., if connecting this node to DN42. /etc/bird/intra/defs.conf: Functions for filters function is_intra_net4() { return net ~ INTRA_PREFIX_V4; } function is_intra_net6(){ return net ~ INTRA_PREFIX_V6; } function is_intra_dn42_net4(){ return net ~ [ 172.20.0.0/14+ ]; } function is_intra_dn42_net6(){ return net ~ [ fd00::/8+ ]; } /etc/bird/intra/kernel.conf: Write routes learned by OSPF into the system routing table protocol kernel intra_kernel_v4 { kernel table 254; scan time 20; ipv4 { table intra_table_v4; import none; export filter { if source = RTS_STATIC then reject; accept; }; }; }; protocol kernel intra_kernel_v6 { kernel table 254; scan time 20; ipv6 { table intra_table_v6; import none; export filter { if source = RTS_STATIC then reject; accept; }; }; }; /etc/bird/intra/ospf.conf: OSPF module protocol ospf v3 intra_ospf_v4 { router id INTRA_ROUTER_ID; # Specify RouterID ipv4 { table intra_table_v4; # Specify routing table import where is_intra_dn42_net4() || is_intra_net4() && source != RTS_BGP; export where is_intra_dn42_net4() || is_intra_net4() && source != RTS_BGP; }; include "ospf/*"; }; protocol ospf v3 intra_ospf_v6 { router id INTRA_ROUTER_ID; # Specify RouterID ipv6 { table intra_table_v6; # Specify routing table import where is_intra_dn42_net6() || is_intra_net6() && source != RTS_BGP; export where is_intra_dn42_net6() || is_intra_net6() && source != RTS_BGP; }; include "ospf/*"; }; /etc/bird/intra/ospf/backbone.conf: OSPF Area Configuration area 0.0.0.0 { interface "br-lan" { stub; }; # Local LAN interface interface "zta7oqfzy6" { # ZeroTier interface type broadcast; cost 100; hello 20; }; }; After completion, use: /etc/init.d/bird start /etc/init.d/bird enable to start Bird and enable auto-start on boot. If everything is fine, you can use birdc s p to check Bird's status. If all goes well, after the other side is configured, you should see the OSPF state as Running: root@X-WRT:/etc/bird# birdc s p BIRD 2.18 ready. Name Proto Table State Since Info device1 Device --- up 14:28:02.410 intra_kernel_v4 Kernel intra_table_v4 up 14:28:02.410 intra_kernel_v6 Kernel intra_table_v6 up 14:28:02.410 intra_ospf_v4 OSPF intra_table_v4 up 14:28:02.410 Running intra_ospf_v6 OSPF intra_table_v6 up 14:31:38.389 Running Have your friend follow the same process. Once both sides show Running status, you can use birdc s r protocol intra_ospf_v4 to view the routes learned by OSPF. You'll find that routes to the other side via ZeroTier are being learned normally: root@X-WRT:/etc/bird# birdc s r protocol intra_ospf_v4 BIRD 2.18 ready. Table intra_table_v4: ... 192.168.1.0/24 unicast [intra_ospf_v4 23:20:21.398] * I (150/110) [100.64.0.163] via 100.64.0.163 on zta7oqfzy6 ... 192.168.3.0/24 unicast [intra_ospf_v4 14:28:02.511] * I (150/10) [100.64.0.100] dev br-lan You can also ping your friend's server from your PC: iyoroy@iYoRoy-PC:~$ ping 192.168.1.103 PING 192.168.1.103 (192.168.1.103) 56(84) bytes of data. 64 bytes from 192.168.1.103: icmp_seq=1 ttl=63 time=54.3 ms 64 bytes from 192.168.1.103: icmp_seq=2 ttl=63 time=10.7 ms 64 bytes from 192.168.1.103: icmp_seq=3 ttl=63 time=15.2 ms ^C --- 192.168.1.103 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1998ms rtt min/avg/max/mdev = 10.678/26.717/54.279/19.576 ms iyoroy@iYoRoy-PC:~$ traceroute 192.168.1.103 traceroute to 192.168.1.103 (192.168.1.103), 30 hops max, 60 byte packets 1 100.64.0.163 (100.64.0.163) 10.445 ms 9.981 ms 9.892 ms 2 192.168.1.103 (192.168.1.103) 11.621 ms 10.994 ms 10.948 ms Web browsing and speed tests work normally: Summary This series of operations essentially implements the following network structure: flowchart TB %% === Style Definitions === classDef phyNet fill:#e3f2fd,stroke:#1565c0,stroke-width:2px classDef virNet fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,stroke-dasharray: 5 5 classDef router fill:#333,stroke:#000,stroke-width:2px,color:#fff classDef ztCard fill:#f57c00,stroke:#e65100,stroke-width:2px,color:#fff,shape:rect classDef bird fill:#a5d6a7,stroke:#2e7d32,stroke-width:1px,color:#000 classDef invisibleContainer fill:none,stroke:none,color:none %% === Physical Layer Containers === subgraph Top_Physical_Layer [" "] direction LR subgraph Left_Side ["My Home (Node A)"] direction TB L_Router[X-WRT Router A]:::router L_LAN[LAN: 192.168.3.0/24] L_LAN <--> L_Router end subgraph Right_Side ["Friend's Home (Node B)"] direction TB R_Router[X-WRT Router B]:::router R_LAN[LAN: 192.168.1.0/24] R_LAN <--> R_Router end end %% === Virtual Layer Container === subgraph Middle_Side [ZeroTier Virtual L2 Network] direction LR subgraph ZT_Stack_A [My Home ZT Access] direction TB L_NIC(zt0: 100.64.0.x):::ztCard L_Bird(Bird OSPF):::bird L_NIC <-.- L_Bird end subgraph ZT_Stack_B [Friend's Home ZT Access] direction TB R_NIC(zt0: 100.64.0.y):::ztCard R_Bird(Bird OSPF):::bird R_NIC <-.- R_Bird end L_NIC <==P2P Tunnel==> R_NIC end %% === Cross-Layer Connections === L_Router === L_NIC R_Router === R_NIC %% === Style Application === class Left_Side,Right_Side phyNet class Middle_Side virNet class Top_Physical_Layer invisibleContainer The underlying P2P network is still powered by ZeroTier. However, using OSPF for internal routing allows both sides to directly route to devices on each other's network segments. Since both sides can fully learn each other's routes, no NAT is required, and both sides can directly see each other's source addresses. Check out the other side of this story! From my friend's side: Linux Operations - OSPF Networking Implementation Based on Bird for New OpenWrt » NanamiのTechLaunchTower
An Experience of Manually Installing Proxmox VE, Configuring Multipath iSCSI, and NAT Forwarding
The reason was that I rented a physical server, but the IDC did not provide Proxmox VE or Debian system images, only Ubuntu, CentOS, and Windows series. Additionally, the data disk was provided via multipath iSCSI. I wanted to use PVE for isolating different usage scenarios, so I attempted to reinstall the system and migrate the aforementioned configurations. Backup Configuration First, perform a general check of the system, which reveals: The system has two Network Interfaces: enp24s0f0 is connected to a public IP address for external access; enp24s0f1 is connected to the private network address 192.168.128.153. The data disk is mapped to /dev/mapper/mpatha. Under /etc/iscsi, there are configurations for two iSCSI Nodes: 192.168.128.250:3260 and 192.168.128.252:3260, both corresponding to the same target iqn.2024-12.com.ceph:iscsi. It can be inferred that the data disk is mounted by configuring two iSCSI Nodes and then merging them into a single device using multipath. Check the system's network configuration: network: version: 2 renderer: networkd ethernets: enp24s0f0: addresses: [211.154.[REDACTED]/24] routes: - to: default via: [REDACTED] match: macaddress: ac:1f:6b:0b:e2:d4 set-name: enp24s0f0 nameservers: addresses: - 114.114.114.114 - 8.8.8.8 enp24s0f1: addresses: - 192.168.128.153/17 match: macaddress: ac:1f:6b:0b:e2:d5 set-name: enp24s0f1 It's found to be very simple static routing. The internal network interface doesn't even have a default route; just binding the IP is sufficient. Then, save the iSCSI configuration files from /etc/iscsi, which include account and password information. Reinstall Debian Used the bin456789/reinstall script for this reinstallation. Download the script: curl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_ Reinstall as Debian 13 (Trixie): bash reinstall.sh debian 13 Then, enter the password you want to set as prompted. If all goes well, wait about 10 minutes, and it will automatically complete and reinstall into a clean Debian 13. You can connect via SSH during the process using the set password to check the installation progress. After reinstalling, perform a source change and apt upgrade as usual to get a clean Debian 13. For changing sources, directly refer to the USTC Mirror Site tutorial. Install Proxmox VE This step mainly refers to the Proxmox official tutorial. Note: The Debian installed by the above script sets the hostname to localhost. If you want to change it, please modify it before configuring the Hostname and change the hostname in hosts to your modified hostname, not localhost. Configure Hostname Proxmox VE requires the current hostname to be resolvable to a non-loopback IP address: The hostname of your machine must be resolvable to an IP address. This IP address must not be a loopback one like 127.0.0.1 but one that you and other hosts can connect to. For example, my server IP is 211.154.[CENSORED], I need to add the following record in /etc/hosts: 127.0.0.1 localhost +211.154.[CENSORED] localhost ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters After saving, use hostname --ip-address to check if it outputs the set non-loopback address: ::1 127.0.0.1 211.154.[CENSORED]. Add Proxmox VE Software Repository Debian 13 uses the Deb822 format (though you can use sources.list if you want), so just refer to the USTC Proxmox Mirror Site: cat > /etc/apt/sources.list.d/pve-no-subscription.sources <<EOF Types: deb URIs: https://mirrors.ustc.edu.cn/proxmox/debian/pve Suites: trixie Components: pve-no-subscription Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg EOF Here, a keyring needs to be migrated but I couldn't find one after searching online, so I chose to pull a copy from an existing Proxmox VE server. It's available here: proxmox-keyrings.zip Extract the public key file and place it in /usr/share/keyrings/, then run: apt update apt upgrade -y This will sync the Proxmox VE software repository. Install Proxmox VE Kernel Use the following command to install the PVE kernel and reboot to apply the new kernel: apt install proxmox-default-kernel reboot Afterwards, uname -r should show a kernel version ending with pve, like 6.17.2-2-pve, indicating the new kernel is successfully applied. Install Proxmox VE Related Packages Use apt to install the corresponding packages: apt install proxmox-ve postfix open-iscsi chrony During configuration, you will need to set up the postfix mail server. Official explanation: If you have a mail server in your network, you should configure postfix as a satellite system. Your existing mail server will then be the relay host which will route the emails sent by Proxmox VE to their final recipient. If you don't know what to enter here, choose local only and leave the system name as is. After this, you should be able to access the Web console at https://<your server address>:8006. The account is root, and the password is your root password, i.e., the password configured during the Debian reinstallation. Remove Old Debian Kernel and os-prober Use the following commands: apt remove linux-image-amd64 'linux-image-6.1*' update-grub apt remove os-prober to remove the old Debian kernel, update grub, and remove os-prober. Removing os-prober is not mandatory, but it is recommended by the official guide because it might mistakenly identify VM boot files as multi-boot files, adding incorrect entries to the boot list. At this point, the installation of Proxmox VE is complete and ready for normal use! Configuring Internal Network Interface Because the iSCSI network interface and the public network interface are different, and the reinstallation lost this configuration, the internal network interface needs to be manually configured. Open the Proxmox VE Web interface, go to Datacenter - localhost (hostname) - Network, edit the internal network interface (e.g., ens6f1 here), enter the backed-up IPv4 in CIDR format: 192.168.128.153/17, and check Autostart, then save. Then use the command to set the interface state to UP: ip link set ens6f1 up Now you should be able to ping the internal iSCSI server's IP. Configure Data Disk iSCSI In the previous step, we should have installed the open-iscsi package required for iscsiadm. We just need to reset the nodes according to the backed-up configuration. First, discover the iSCSI storage: iscsiadm -m discovery -t st -p 192.168.128.250:3260 This should yield the two original LUN Targets: 192.168.128.250:3260,1 iqn.2024-12.com.ceph:iscsi 192.168.128.252:3260,2 iqn.2024-12.com.ceph:iscsi Transfer the backed-up configuration files to the server, overwriting the existing configuration in /etc/iscsi. Also, in my backed-up config, I found the authentication configuration: # /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.250,3260,1/default # BEGIN RECORD 2.1.5 node.name = iqn.2024-12.com.ceph:iscsi ... # Some unimportant configurations omitted node.session.auth.authmethod = CHAP node.session.auth.username = [CENSORED] node.session.auth.password = [CENSORED] node.session.auth.chap_algs = MD5 ... # Some unimportant configurations omitted # /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.252,3260,2/default # BEGIN RECORD 2.1.5 node.name = iqn.2024-12.com.ceph:iscsi ... # Some unimportant configurations omitted node.session.auth.authmethod = CHAP node.session.auth.username = [CENSORED] node.session.auth.password = [CENSORED] node.session.auth.chap_algs = MD5 ... # Some unimportant configurations omitted Write these configurations to the new system using: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.authmethod -v CHAP iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.username -v [CENSORED] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.password -v [CENSORED] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5 iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.authmethod -v CHAP iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.username -v [CENSORED] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.password -v [CENSORED] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5 (I don't know why the auth info needs to be written separately, but testing shows it won't log in without rewriting it.) Then, use: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 --login iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 --login to log into the Targets. Then use: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.startup -v automatic iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.startup -v automatic to enable automatic mounting on boot. At this point, checking disks with tools like lsblk should reveal two additional hard drives; in my case, sdb and sdc appeared. Configure Multipath To identify if it's a multipath device, I tried: /usr/lib/udev/scsi_id --whitelisted --device=/dev/sdb /usr/lib/udev/scsi_id --whitelisted --device=/dev/sdc Checking the scsi_id of the two disk devices revealed they were identical, confirming they are the same disk using multi-path for load balancing and failover. Install multipath-tools using apt: apt install multipath-tools Then, create /etc/multipath.conf and add: defaults { user_friendly_names yes find_multipaths yes } Configure multipathd to start on boot: systemctl start multipathd systemctl enable multipathd Then, use the following command to scan and automatically configure the multipath device: multipath -ll It should output: mpatha(360014056229953ef442476e85501bfd7)dm-0LIO-ORG,TCMU device size=500G features='1 queue_if_no_path' hwhandler='1 alua'wp=rw |-+- policy='service-time 0' prio=50 status=active | `- 14:0:0:152 sdb 8:16 active ready running `-+- policy='service-time 0' prio=50 status=active `- 14:0:0:152 sdc 8:16 active ready running This shows the two disks have been recognized as a single multipath device. Now, you can find the multipath disk under /dev/mapper/: root@localhost:/dev/mapper# ls control mpatha mpatha is the multipath aggregated disk. If it's not scanned, try using: rescan-scsi-bus.sh to rescan the SCSI bus and try again. If the command is not found, install it via apt install sg3-utils. If all else fails, just reboot. Configure Proxmox VE to Use the Data Disk Because we used multipath, we cannot directly add an iSCSI type storage. Use the following commands to create the PV and VG: pvcreate /dev/mapper/mpatha vgcreate <vg name> /dev/mapper/mpatha Here, I configured the entire disk as a PV. You could also create a separate partition for this. After completion, open the Proxmox VE management interface, go to Datacenter - Storage, click Add - LVM, select the name of the VG you just created for Volume group, give it an ID (name), and click Add. At this point, all configurations from the original system should have been migrated. Configure NAT and Port Forwarding NAT Because only one IPv4 address was purchased, NAT needs to be configured to allow all VMs to access the internet normally. Open /etc/network/interfaces and add the following content: auto vmbr0 iface vmbr0 inet static address 192.168.100.1 netmask 255.255.255.0 bridge_ports none bridge_stp off bridge_fd 0 post-up echo 1 > /proc/sys/net/ipv4/ip_forward post-up iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE post-up iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1 post-up iptables -A FORWARD -i vmbr0 -j ACCEPT post-down iptables -t nat -D POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1 post-down iptables -D FORWARD -i vmbr0 -j ACCEPT Here, vmbr0 is the NAT bridge, with the IP segment 192.168.100.0/24. Traffic from this segment will be translated to the IP of the external network interface ens6f0 for outgoing traffic, and translated back to the original IP upon receiving replies, enabling IP sharing. Then, use: ifreload -a to reload the configuration. Now, the VMs should be able to access the internet. Just configure a static IP within the 192.168.100.0/24 range during installation, set the default gateway to 192.168.100.1, and configure the DNS address. Port Forwarding Got lazy, directly prompted an AI. Had an AI write a configuration script /usr/local/bin/natmgr: #!/bin/bash # =================Configuration Area================= # Public network interface name (Please modify according to your actual situation) PUB_IF="ens6f0" # ==================================================== ACTION=$1 ARG1=$2 ARG2=$3 ARG3=$4 ARG4=$5 # Check if running as root if [ "$EUID" -ne 0 ]; then echo "Please run this script with root privileges" exit 1 fi # Generate random ID (6 characters) generate_id() { # Introduce nanoseconds and random salt to ensure ID uniqueness even if the script runs quickly echo "$RANDOM $(date +%s%N)" | md5sum | head -c 6 } # Show help information usage() { echo "Usage: $0 {add|del|list|save} [parameters]" echo "" echo "Commands:" echo " add <Public Port> <Internal IP> <Internal Port> [Protocol] Add forwarding rule" echo " [Protocol] optional: tcp, udp, both (default: both)" echo " del <ID> Delete forwarding rule by ID" echo " list View all current forwarding rules" echo " save Save current rules to persist after reboot (Must run!)" echo "" echo "Examples:" echo " $0 add 8080 192.168.100.101 80 both" echo " $0 save" echo "" } # Internal function: add single protocol rule _add_single_rule() { local PROTO=$1 local L_PORT=$2 local T_IP=$3 local T_PORT=$4 local RULE_ID=$(generate_id) local COMMENT="NAT_ID:${RULE_ID}" # 1. Add DNAT rule (PREROUTING chain) iptables -t nat -A PREROUTING -i $PUB_IF -p $PROTO --dport $L_PORT -j DNAT --to-destination $T_IP:$T_PORT -m comment --comment "$COMMENT" # 2. Add FORWARD rule (Allow packet passage) iptables -A FORWARD -p $PROTO -d $T_IP --dport $T_PORT -m comment --comment "$COMMENT" -j ACCEPT # Output result printf "%-10s %-10s %-10s %-20s %-10s\n" "$RULE_ID" "$PROTO" "$L_PORT" "$T_IP:$T_PORT" "Success" # Remind user to save echo "Please run '$0 save' to ensure rules persist after reboot." } # Main add function add_rule() { local L_PORT=$1 local T_IP=$2 local T_PORT=$3 local PROTO_REQ=${4:-both} # Default to both if | -z "$T_IP" || -z "$T_PORT" ; then echo "Error: Missing parameters" usage exit 1 fi # Convert to lowercase PROTO_REQ=$(echo "$PROTO_REQ" | tr '[:upper:]' '[:lower:]') echo "Adding rule..." printf "%-10s %-10s %-10s %-20s %-10s\n" "ID" "Protocol" "Public Port" "Target Address" "Status" echo "------------------------------------------------------------------" if "$PROTO_REQ" == "tcp" ; then _add_single_rule "tcp" "$L_PORT" "$T_IP" "$T_PORT" elif "$PROTO_REQ" == "udp" ; then _add_single_rule "udp" "$L_PORT" "$T_IP" "$T_PORT" elif "$PROTO_REQ" == "both" ; then _add_single_rule "tcp" "$L_PORT" "$T_IP" "$T_PORT" _add_single_rule "udp" "$L_PORT" "$T_IP" "$T_PORT" else echo "Error: Unsupported protocol '$PROTO_REQ'. Please use tcp, udp, or both." exit 1 fi echo "------------------------------------------------------------------" } # Delete rule (Delete in reverse line number order) del_rule() { local RULE_ID=$1 if -z "$RULE_ID" ; then echo "Error: Please provide rule ID" usage exit 1 fi echo "Searching for rule with ID [${RULE_ID}]..." local FOUND=0 # --- Clean NAT table (PREROUTING) --- LINES=$(iptables -t nat -nL PREROUTING --line-numbers | grep "NAT_ID:${RULE_ID}" | awk '{print $1}' | sort -rn) if ! -z "$LINES" ; then for line in $LINES; do iptables -t nat -D PREROUTING $line echo "Deleted NAT table PREROUTING chain line $line" FOUND=1 done fi # --- Clean Filter table (FORWARD) --- LINES=$(iptables -t filter -nL FORWARD --line-numbers | grep "NAT_ID:${RULE_ID}" | awk '{print $1}' | sort -rn) if ! -z "$LINES" ; then for line in $LINES; do iptables -t filter -D FORWARD $line echo "Deleted Filter table FORWARD chain line $line" FOUND=1 done fi if $FOUND -eq 0 ; then echo "No rule found with ID $RULE_ID." else echo "Delete operation completed." echo "Please run '$0 save' to update the persistent configuration file." fi } # Save rules to disk (New feature) save_rules() { echo "Saving current iptables rules..." # netfilter-persistent is the service managing iptables-persistent in Debian/Proxmox if command -v netfilter-persistent &> /dev/null; then netfilter-persistent save if [ $? -eq 0 ]; then echo "✅ Rules successfully saved to /etc/iptables/rules.v4, will be automatically restored after system reboot." else echo "❌ Failed to save rules. Please check the status of the 'netfilter-persistent' service." fi else echo "Warning: 'netfilter-persistent' command not found." echo "Please ensure the 'iptables-persistent' package is installed." echo "Install command: apt update && apt install iptables-persistent" fi } # List rules list_rules() { echo "Current Port Forwarding Rules List:" printf "%-10s %-10s %-10s %-20s %-10s\n" "ID" "Protocol" "Public Port" "Target Address" "Target Port" echo "------------------------------------------------------------------" # Parse iptables output iptables -t nat -nL PREROUTING -v | grep "NAT_ID:" | while read line; do id=$(echo "$line" | grep -oP '(?<=NAT_ID:)[^ ]*') # Extract protocol if echo "$line" | grep -q "tcp"; then proto="tcp"; else proto="udp"; fi # Extract port after dpt: l_port=$(echo "$line" | grep -oP '(?<=dpt:)[0-9]+') # Extract IP:Port after to: target=$(echo "$line" | grep -oP '(?<=to:).*') t_ip=${target%:*} t_port=${target#*:} printf "%-10s %-10s %-10s %-20s %-10s\n" "$id" "$proto" "$l_port" "$t_ip" "$t_port" done } # Main logic case "$ACTION" in add) add_rule "$ARG1" "$ARG2" "$ARG3" "$ARG4" ;; del) del_rule "$ARG1" ;; list) list_rules exit 0 ;; save) save_rules ;; *) usage exit 1 ;; esac save_rules This script automatically adds/deletes iptables rules for port forwarding. Remember to chmod +x. Use iptables-persistent to save the configuration and load it automatically on boot: apt install iptables-persistent During configuration, you will be asked whether to save the current rules; Yes or No is fine. When adding a forwarding rule, use natmgr add <host listen port> <VM internal IP> <VM port> [tcp/udp/both]. The script will automatically assign a unique ID. Use natmgr del <ID> to delete. Use natmgr list to view the existing forwarding list. Reference Articles: bin456789/reinstall: 一键DD/重装脚本 (One-click reinstall OS on VPS) - GitHub Install Proxmox VE on Debian 12 Bookworm - Proxmox VE PVE连接 TrueNAS iSCSI存储实现本地无盘化_pve iscsi-CSDN博客 ProxmoxVE (PVE) NAT 网络配置方法 - Oskyla 烹茶室
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
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
Configuring Multi-Exit Routing for a Dual-NIC VPS Using PBR
Warning: This is a low-value/watered-down post Background I got a Shenzhen-Hong Kong IEPL machine from a friend. It has two network interfaces, eth0 and eth1, but by default, all traffic goes through eth0. eth1 had no routing configured. I planned to use metrics for basic traffic splitting initially, and then use PBR (Policy-Based Routing) to implement rule-based routing configuration. Configuration The machine's network was initially configured by cloud-init: # This file is generated from information provided by the datasource. Changes # to it will not persist across an instance reboot. To disable cloud-init's # network configuration capabilities, write a file # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: # network: {config: disabled} network: version: 2 ethernets: eth0: addresses: - 10.10.1.31/16 gateway4: 10.10.0.1 match: macaddress: bc:24:11:f8:42:7a nameservers: addresses: - 223.5.5.5 - 119.29.29.29 search: - [REDACTED] set-name: eth0 eth1: addresses: - 10.20.1.31/16 - [REDACTED]/64 gateway4: 10.20.0.1 gateway6: fe80::be24:11ff:fe80:66bb match: macaddress: bc:24:11:50:96:0a nameservers: addresses: - 223.5.5.5 - 119.29.29.29 search: - [REDACTED] set-name: eth1 After backing up the config, I added metrics: # This file is generated from information provided by the datasource. Changes # to it will not persist across an instance reboot. To disable cloud-init's # network configuration capabilities, write a file # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: # network: {config: disabled} network: version: 2 ethernets: eth0: addresses: - 10.10.1.31/16 gateway4: 10.10.0.1 match: macaddress: bc:24:11:f8:42:7a nameservers: addresses: - 223.5.5.5 - 119.29.29.29 search: - [REDACTED] set-name: eth0 + routes: + - to: "default" + via: "10.10.0.1" + # Set metric=50 as the backup exit + metric: 50 eth1: addresses: - 10.20.1.31/16 - [REDACTED]/64 gateway4: 10.20.0.1 gateway6: fe80::be24:11ff:fe80:66bb match: macaddress: bc:24:11:50:96:0a nameservers: addresses: - 223.5.5.5 - 119.29.29.29 search: - [REDACTED] set-name: eth1 + routes: + - to: "default" + via: "10.20.0.1" + # Set metric=25 as the preferred exit + metric: 25 Create the PBR configuration: # /etc/netplan/90-pbr.yaml network: version: 2 ethernets: eth0: routes: - to: default via: 10.10.0.1 table: 10 routing-policy: - from: 10.10.0.0/16 table: 10 - to: 202.46.[REDACTED]/32 table: 10 eth1: routes: - to: default via: 10.20.0.1 table: 20 routing-policy: - from: 10.20.0.0/16 table: 20 - to: 38.47.[REDACTED]/32 table: 20 - to: 23.149.[REDACTED]/32 table: 20 For IPs that need to be accessed via a specific exit, just add a 'to' type rule and bind it to the corresponding routing table. After finishing, run netplan apply to update the configuration.
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
Introduction This article documents my complete process of applying for an ASN through the RIPE NCC. The content is suitable for beginners. If you find any errors, please feel free to contact me via email, and I will correct them promptly. Now that we've learned the basic BGP concepts on DN42, it seems a bit of a waste not to play with the public internet, right? Basic Concepts Currently, the allocation of public ASN and IP resources is managed by five Regional Internet Registries (RIRs) worldwide: ARIN: Manages the North American region. RIPE NCC: Manages the European region. APNIC: Manages the Asia-Pacific region. LACNIC: Manages the Latin American region. AfriNIC: Manages the African region. RIRs do not provide services directly to end users. Instead, they allocate resources to Local Internet Registries (LIRs), which then assign them to end users. Of course, individual users can also register as an LIR, but this is generally not cost-effective. If you're willing to pay thousands of dollars in annual fees, then forget I said that. Among these, RIPE NCC is considered more friendly towards individual applications, followed by ARIN and APNIC. Compared to RIPE NCC, APNIC's fees are generally about 30% higher. Furthermore, RIPE NCC provides an online management system allowing users to modify information and check progress themselves, whereas with APNIC, you typically need to contact an LIR for changes. Overall, I chose to apply for an ASN through the RIPE NCC. The resources obtained (both ASN and IPs) are generally categorized into two types: PA (Provider Aggregatable) Resources: Belong to the LIR and are assigned for your use by the LIR. PI (Provider Independent) Resources: Belong to you directly. These are generally more expensive. Preparation Stage Choosing an LIR Search online for LIR Service to find many companies offering such services. Currently, RIPE NCC charges an annual administrative fee of 50 EUR for PI resources. This means the cost from an LIR for registering an ASN generally won't be lower than 50 EUR per year (approximately 60 USD at the time of writing). Here, I chose NoPKT LLC, recommended by peers. Their pricing is quite reasonable and includes a /48 block of PA IPv6 addresses with the ASN. The activation speed was also very fast – it only took half a day from submitting the required documents to getting the ASN. Preparing Documents Proof of Identity Individual: Provide an ID card or passport (I submitted photos of the front and back of my national ID card). Company: Provide a valid business license. If the applicant is a minor, usually written consent from their legal guardian is required, and the guardian must fulfill corresponding responsibilities. All submitted documents must be authentic and valid, and should be originals or notarized copies. Contact Information Postal Address: Used for registration in the RIPE Database. Technical Contact Email. Abuse Contact Email. Technical Justification Billing from a BGP-capable provider within the European region. Options include Vultr, BuyVM, iFog, V.PS, etc. Note: Vultr uses a post-payment system, generating invoices at the beginning of the month. If you need the documents ready quickly, consider other providers. ASNs of two upstream providers you plan to connect to. (In practice, the reviewers won't strictly verify the specific upstream ASNs you list. Therefore, you can fill in common, publicly known ASNs to make it look reasonable. Don't overthink it too much. You can even put mine.) Registering a RIPE DB Account and Creating Objects Go to the RIPE Database and register an account. For Chinese, it's recommended to use the 拼音(Pinyin) of your real legal name. Enabling 2FA is mandatory, so please install a TOTP app on your phone beforehand. Creating a Role Object and Maintainer Object Go to Create role and maintainer pair - RIPE Database to create a role object. Here, a 'role' is an abstract concept describing the contact information for a team, department, or functional role – it represents a role, such as NOC (Network Operations Center), Abuse Team, Hostmaster, etc. mntner: The identifier for the maintainer object. It can contain uppercase/lowercase letters, numbers, and -_. For example, I used IYOROY-MNT. role: The name for the role object. It can contain uppercase/lowercase letters, numbers, and ][)(._"*@,&:!'+/-. For example, I used IYOROY-NETWORK-NOC. address: The office address for this role. e-mail: The email address for this role. Click SUBMIT after filling out the form to create both the role object and the maintainer object. Please note the returned primary key name, which usually ends with -RIPE. You will need this for future modifications and submissions to the LIR. The maintainer object identifier here is conceptually different from the role object. The maintainer signifies who has the authority to maintain (create/modify/delete) objects in the database – it's the maintaining entity. The relationship between different concepts in the RIPE Database can be referenced in the diagram later in the article. Adding an Abuse Contact Mailbox Go to Query - RIPE Database and search for the primary key of the role you just created. You should find the entry you created. Click "Update Object" on the right. Click the plus sign (+) next to the email field to add an abuse-mailbox attribute and fill in your abuse contact email address: Click SUBMIT to save. Note: RIPE periodically checks if the abuse-mailbox is functional. Please ensure you provide a real, active email address. Creating an Organization Object The Organization object here is an abstraction of a legal entity or organization (company, university, ISP, individual user, etc.). It serves as the top-level ownership information for resource objects (like aut-num, inetnum, inet6num) in the RIPE Database. This means subsequent ASN and IP resources will be assigned to this Organization object. Go to Create Organization - RIPE Database and fill in the following information: organisation: A unique ID. Keep it as the default AUTO-1 to let RIPE NCC assign one. org-name: The name of the organization. For Chinese individuals, use your full name in Pinyin. address: Postal address. country: Country code, refer to ISO 3166. For China, use CN. e-mail: The organization's email address. admin-c / tech-c: Administrative and technical contact objects (referencing the role handle). abuse-c: Specifies the abuse contact (must be a role object linked to the abuse-mailbox in that role). mnt-ref: Specifies which maintainer(s) can create objects referencing this organisation. mnt-by: Specifies who can maintain this organisation object itself. Click SUBMIT after filling out the form and note the returned object identifier, which follows a format like ORG-XXXX-RIPE. If you need to make changes after submission, go to Query - RIPE Database and search for the previously noted Role primary key or the Organization object identifier to find the update option. Paying the LIR Fee and Submitting Documents Submit the following documents to your chosen LIR: Proof of Identity Full Name Address (recommended to match your ID document) Photos of the front and back of your ID card RIPE Database Information org: Organization object identifier as-name: AS Name admin-c: Primary key of the role object created earlier tech-c: Primary key of the role object created earlier abuse-c: Primary key of the role object created earlier nic-hdl: Primary key of the role object created earlier mnt-by: Name of the maintainer object created earlier Technical Justification VPS Bill/Invoice Upstream ASNs The LIR will likely ask you to add a mnt-ref attribute to your Organization object, pointing to the LIR's maintainer. This allows the LIR to assign the AS and IP resources to your Organization. Once the LIR reviews and approves your application, they will submit the request to RIPE. Then, it's a waiting game. Generally, it takes 3-5 working days to get your ASN. At this point, we have successfully registered our own ASN on the public internet. Supplement: Relationships Between Concepts in the RIPE Database graph LR %% ========== ORG Layer ========== subgraph Org["Organisation"] ORG["organisation\n(ORG-XXX-RIPE)"] end %% ========== Resource Layer ========== subgraph Resource["Resources"] INETNUM["inetnum\n(IPv4 Block)"] INET6NUM["inet6num\n(IPv6 Block)"] AUTNUM["aut-num\n(ASN)"] ASSET["as-set\n(ASN Set)"] end %% ========== Routing Layer ========== subgraph Routing["Routing"] ROUTE["route\n(IPv4 Route Announcement)"] ROUTE6["route6\n(IPv6 Route Announcement)"] end %% ========== Contact Layer ========== subgraph Contact["Contacts"] ROLE["role\n(Team/Function)\nnic-hdl"] PERSON["person\n(Individual)\nnic-hdl"] end %% ========== Authorization Layer ========== subgraph Maintainer["Authorization"] MNT["mntner\n(Maintainer)"] end %% ========== Contact Links ========== INETNUM --> ROLE INET6NUM --> ROLE AUTNUM --> ROLE ASSET --> ROLE ROUTE --> ROLE ROUTE6 --> ROLE ROLE --> PERSON %% ========== Organization Assignment ========== ORG --> INETNUM ORG --> INET6NUM ORG --> AUTNUM ORG --> ASSET %% ========== Authorization ========== ORG --> MNT INETNUM --> MNT INET6NUM --> MNT AUTNUM --> MNT ASSET --> MNT ROUTE --> MNT ROUTE6 --> MNT ROLE --> MNT PERSON --> MNT %% ========== Route Binding ========== ROUTE -->|origin| AUTNUM ROUTE6 -->|origin| AUTNUM %% ========== Route Scope ========== ROUTE -->|belongs to| INETNUM ROUTE6 -->|belongs to| INET6NUM Special thanks to Mi Lu for providing technical support and answering questions! Reference Articles: 自己在家开运营商 Part.1 - 注册一个 ASN - LYC8503 从0开始注册一个ASN并广播IP | Pysio's Home 青年人的第一个运营商：注册一个 ASN | liuzhen932 的小窝
Foreword I am a novice in BGP. This article may contain imprecise content/naive understandings/elementary mistakes. I kindly ask the experts to be lenient. If you find any issues, you are welcome to contact me via email, and I will correct them as soon as possible. If you find this unacceptable, it is recommended to close this article now. What are BGP Communities? TL;DR: BGP Communities "tag" routes, allowing others to use these tags for route optimization. This concept might be unfamiliar to newcomers (like me), who might not initially understand its purpose. Simply put, BGP Communities are a mechanism for tagging routes, similar to adding labels. They allow network administrators to attach one or more "tags" (i.e., community values) to routes propagated via BGP. These tags themselves do not alter the route's path attributes (like AS_PATH, LOCAL_PREF, MED, etc.), but they provide a signaling mechanism to indicate what policy or processing should be applied to that route by other routers within the same AS or in downstream peer ASes. BGP Communities can be used to: Simplify Policy Configuration: Routers inside the network or in downstream ASes only need to configure policies based on community values (like setting LOCAL_PREF, adding NO_EXPORT, applying route-maps, etc.), without needing to know the specific prefix details. This makes policies more centralized, easier to manage, and less prone to errors. Convey Policy Intent to Downstream ASes: An AS can attach community values to routes it advertises to its downstream customer or peer ASes. These communities convey requirements or suggestions on how these routes should be handled, such as route optimization based on geographic location, latency, or bandwidth. Coordinate Policies within an AS: Inside a large AS, when using IBGP full-mesh or route reflectors, edge routers (receiving EBGP routes or redistributing routes) can tag routes with community values. Core routers or route reflectors within the AS can recognize these communities and apply corresponding internal policies (like setting LOCAL_PREF, MED, deciding whether to advertise to certain IBGP peers, adding other communities, etc.), without needing complex prefix-based policies on every internal router. DN42 has its own set of Communities specifications. For details, please refer to: BGP-communities - DN42 Wiki Configuration The general idea and approach in this article are largely based on Xe_iu's method, focusing on adding BGP Communities for geographic information and performing route optimization. Generally, this is sufficient. (Another reason is that I haven't fully grasped the others yet) Note: We should ONLY add geographic information-related BGP Communities to routes originating from our own AS. We should not add such entries to routes received from neighbors. Adding our own regional Communities to a neighbor's routes constitutes forging the route origin, potentially leading to route hijacking. Downstream networks might misjudge the traffic path, routing traffic that should be direct through your network, increasing latency and consuming your network's bandwidth. (Large Communities are an exception, as they have a verification mechanism to prevent this, but that's beyond the scope of this article). The idea is clear. When exporting routes, we first need to verify if it's our own route. If it is, we add the communities tag to the route. The sample configuration provided by DN42 already includes two functions, is_self_net() and is_self_net_v6(), to check if a route is our own. Therefore, writing the configuration part is straightforward. Adding Communities to Routes First, we need to define the geographic region information for the current node at the beginning of the node configuration file. Please check BGP-communities - DN42 Wiki for details: define DN42_REGION = 52; # 52 represents East Asia define DN42_COUNTRY= 1344; # 1344 represents Hong Kong Please modify these values according to your node's actual geographic location. Then, modify the export filter in the dnpeers template: }; export filter { - if is_valid_network() && source ~ [RTS_STATIC, RTS_BGP] then accept; + if is_valid_network() && source ~ [RTS_STATIC, RTS_BGP] then{ + if (is_self_net()) then { # Check if it's our own route + bgp_community.add((64511, DN42_REGION)); # Add continent-level region info + bgp_community.add((64511, DN42_COUNTRY)); # Add country/region info + } + accept; + } reject; }; import limit 1000 action block; During export, it checks if it's our own route. If it is, it sets the bgp_community according to the defined DN42_REGION and DN42_COUNTRY. Here, 64511 is the reserved public AS number identifier specifically for geographic tags (Region/Country); you can just copy it. Apply the same method to the IPv6 export rules, but replace is_self_net() with is_self_net_v6(). Route Optimization Based on Communities Here we need to introduce another concept: local_pref (Local Preference). It is used within an AS to indicate the priority of a route. Its default value is 100, and a higher value indicates higher priority. Furthermore, in BGP route selection logic, local_pref has the highest priority, even higher than AS_PATH length. This means that by setting local_pref, we can adjust the priority of routes to achieve route optimization. Combining this with the BGP Communities mentioned above, we can set the corresponding local_pref based on Communities for optimization. Also, since BGP.local_pref is propagated within the AS, we need to modify the import logic for both eBGP and iBGP routes. My logic for handling BGP.local_pref here references (basically copies) Xe_iu's approach: For routes from the same region, priority +10 For routes from the same country, priority +5 additionally For routes received via direct peering with us, priority +20 additionally Create a function to calculate the priority: function ebgp_calculate_priority() { int priority = 100; # Base priority # Same region detection (+10) if bgp_community ~ [(64511, DN42_REGION)] then priority = priority + 10; # Same country detection (+5) if bgp_community ~ [(64511, DN42_COUNTRY)] then priority = priority + 5; # Direct eBGP neighbor detection (+20) if bgp_path.len = 1 then priority = priority + 20; return priority; } Then, in the import filter within the dnpeers template, set bgp_local_pref to the value calculated by the function: template bgp dnpeers { local as OWNAS; path metric 1; ipv4 { import filter { if is_valid_network() && !is_self_net() then { if (roa_check(dn42_roa, net, bgp_path.last) != ROA_VALID) then { print "[dn42] ROA check failed for ", net, " ASN ", bgp_path.last; reject; } + bgp_local_pref = ebgp_calculate_priority(); accept; } reject; }; Apply the same method for IPv6. After running birdc configure, we should be able to see that our routes have been tagged with Communities labels at our neighbors: (Screenshot source:https://lg.milu.moe/route_all/hk/172.20.234.224) Special thanks to Nuro Trace and Xe_iu. They helped deepen my understanding of BGP Communities and provided much assistance. Reference Articles: [DN42] bird2的配置文件 – Xe_iu's Blog | Xe_iu的杂物间 [DN42] 谈一谈如何配置 BGP community – Xe_iu's Blog | Xe_iu的杂物间 BGP-communities - DN42 Wiki
DN42 - Ep.3 Registering a Domain and Setting Up Authoritative DNS in DN42
Foreword I am a novice in BGP. This article may contain imprecise content/naive understandings/elementary mistakes. If you find any issues, you are welcome to contact me via email, and I will correct them as soon as possible. If you find this unacceptable, it is recommended to close this article now. Assumption: You have already joined DN42, can normally send and receive routing tables, and can access IPs within DN42. Article Update Log {timeline} {timeline-item color="#50BFFF"} August 3, 2025: First edition published. {/timeline-item} {timeline-item color="#4F9E28"} March 15, 2026: Update typos. Thanks to @Auride. {/timeline-item} {/timeline} Motivation While debugging the network, I noticed that pinging or tracerouting others' DN42 IPs could display the reverse-resolved domain names, making it clear which nodes the route passed through, rather than just looking at IPs (as shown in the figure below). It's very intuitive, letting others see at a glance if you've taken a detour. Therefore, I decided to register my own DN42 domain and set up an authoritative DNS service. After reading Lantian's article, I saw he used a PowerDNS + MySQL master-slave synchronization solution. However, my server has limited performance (only 1 core, 1GB RAM), so I plan to use KnotDNS as the DNS server, utilizing the standard zone transfer protocol (AXFR/IXFR) for master-slave synchronization. Preparations {alert type="warning"} The domain names and IPs mentioned in this and subsequent chapters are my own. Please replace them with your own during actual deployment; values enclosed in angle brackets need to be changed according to your requirements. {/alert} I chose the domain: yori.dn42, and plan to deploy DNS servers on three machines: 172.20.234.225, fd18:3e15:61d0::1, ns1.yori.dn42 172.20.234.227, fd18:3e15:61d0::3, ns2.yori.dn42 172.20.234.229, fd18:3e15:61d0::5, ns3.yori.dn42 Among them, ns1.yori.dn42 will be the master node, and ns2, ns3 will be the slave nodes. Installing KnotDNS If port 53 on the system is occupied by a process like systemd-resolvd, disable it first: systemctl stop systemd-resolved systemctl disable systemd-resolved unlink /etc/resolv.conf echo "nameserver 8.8.8.8" > /etc/resolv.conf I am using Debian 12, so I'll use APT for installation: apt install knot knot-dnsutils -y Set KnotDNS to start automatically: systemctl enable knot Configuring KnotDNS Creating a Key First, create a key for synchronization: keymgr -t key_knsupdate Copy the output: # hmac-sha256:key_knsupdate:<your secret> key: - id: key_knsupdate algorithm: hmac-sha256 secret: <your secret> Editing the Configuration File Master Node Edit /etc/knot/knot.conf and fill in the following content: server: rundir: "/run/knot" user: knot:knot automatic-acl: on listen: [ <listen_address1>@53, <listen_address2>@53, ... ] log: - target: syslog any: info database: storage: "/var/lib/knot" ### Paste the Key generated in the previous step here # hmac-sha256:key_knsupdate:<your secret> key: - id: key_knsupdate algorithm: hmac-sha256 secret: <your secret> remote: - id: <DNS_Node_1_ID> address: <DNS_Node_1_IP>@53 - id: <DNS_Node_2_ID> address: <DNS_Node_2_IP>@53 - id: <DNS_Node_3_ID> address: <DNS_Node_3_IP>@53 acl: - id: acl_slave key: key_knsupdate action: transfer - id: acl_master key: key_knsupdate action: notify - id: acl_knsupdate key: key_knsupdate action: update template: - id: default storage: "/var/lib/knot" file: "%s.zone" zone: - domain: <DN42 Domain> notify: [ <Slave_Node_1_ID>, <Slave_Node_2_ID> ] acl: [ acl_slave, acl_knsupdate ] - domain: <IPv4 Reverse Lookup Domain> notify: [ <Slave_Node_1_ID>, <Slave_Node_2_ID> ] acl: [ acl_slave, acl_knsupdate ] - domain: <IPv6 Reverse Lookup Domain> notify: [ <Slave_Node_1_ID>, <Slave_Node_2_ID> ] acl: [ acl_slave, acl_knsupdate ] The listen addresses should include the machine's DN42 IPv4 and DN42 IPv6 addresses. For local debugging, you can add internal IPs like 127.0.0.1 and ::1. The Slave Node IDs are the IDs set in the remote section for the servers you designated as slave nodes. The address in remote can be an internal address, DN42 IPv4, or DN42 IPv6, used only for master-slave synchronization. If using an internal address, add it to the listen list. The template section sets the Zone file storage location to /var/lib/knot. The IPv4 Reverse Lookup Domain should follow the format specified in RFC 2317 based on your allocated IPv4 block. For example, my IPv4 block is 172.20.234.224/28, so my IPv4 reverse lookup domain should be 224/28.234.20.172.in-addr.arpa. This treats the last octet 224/28 as a whole, reverses the order of the remaining parts, and appends .in-addr.arpa. The IPv6 Reverse Lookup Domain should follow the format specified in RFC 3152 based on your allocated IPv6 block. For example, my IPv6 block is fd18:3e15:61d0::/48, so my IPv6 reverse lookup domain should be 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa. This involves reversing the order of the nibbles in the network prefix (excluding the /48) and appending .ip6.arpa. Pad with zeros if necessary. {collapse} {collapse-item label="Example"} server: rundir: "/run/knot" user: knot:knot automatic-acl: on listen: [ 172.20.234.225@53, fd18:3e15:61d0::1@53, localhost@53, 127.0.0.1@53 ] log: - target: syslog any: info database: storage: "/var/lib/knot" # hmac-sha256:key_knsupdate:<key> key: - id: key_knsupdate algorithm: hmac-sha256 secret: <key> remote: - id: 225 # Master Node address: 172.20.234.225@53 - id: 227 # Slave Node address: 172.20.234.227@53 - id: 229 # Slave Node address: 172.20.234.229@53 acl: - id: acl_slave key: key_knsupdate action: transfer - id: acl_master key: key_knsupdate action: notify - id: acl_knsupdate key: key_knsupdate action: update template: - id: default storage: "/var/lib/knot" file: "%s.zone" zone: - domain: yori.dn42 notify: [ 227, 229 ] acl: [ acl_slave, acl_knsupdate ] - domain: 224/28.234.20.172.in-addr.arpa notify: [ 227, 229 ] acl: [ acl_slave, acl_knsupdate ] - domain: 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa notify: [ 227, 229 ] acl: [ acl_slave, acl_knsupdate ] {/collapse-item} {/collapse} Slave Nodes The configuration for slave nodes is largely similar to the master node. Just change the listen addresses to the slave node's addresses and modify the zone section configuration as follows: --- a/knot.conf +++ b/knot.conf zone: - domain: <DN42 Domain> - notify: [ <Slave_Node_1_ID>, <Slave_Node_2_ID> ] - acl: [ acl_slave, acl_knsupdate ] + master: <Master_Node_ID> + zonefile-load: whole + acl: acl_master - domain: <IPv4 Reverse Lookup Domain> - notify: [ <Slave_Node_1_ID>, <Slave_Node_2_ID> ] - acl: [ acl_slave, acl_knsupdate ] + master: <Master_Node_ID> + zonefile-load: whole + acl: acl_master - domain: <IPv6 Reverse Lookup Domain> - notify: [ <Slave_Node_1_ID>, <Slave_Node_2_ID> ] - acl: [ acl_slave, acl_knsupdate ] + master: <Master_Node_ID> + zonefile-load: whole + acl: acl_master The Master Node ID is the ID set in the remote section for the server you designated as the master node. {collapse} {collapse-item label="Examp;e"} server: rundir: "/run/knot" user: knot:knot automatic-acl: on listen: [ 172.20.234.227@53, fd18:3e15:61d0::3@53, localhost@53, 127.0.0.1@53 ] log: - target: syslog any: info database: storage: "/var/lib/knot" # hmac-sha256:key_knsupdate:<key> key: - id: key_knsupdate algorithm: hmac-sha256 secret: <key> remote: - id: 225 address: 172.20.234.225@53 - id: 227 address: 172.20.234.227@53 - id: 229 address: 172.20.234.229@53 acl: - id: acl_slave key: key_knsupdate action: transfer - id: acl_master key: key_knsupdate action: notify - id: acl_knsupdate key: key_knsupdate action: update template: - id: default storage: "/var/lib/knot" file: "%s.zone" zone: - domain: yori.dn42 master: 225 zonefile-load: whole acl: acl_master - domain: 224/28.234.20.172.in-addr.arpa master: 225 zonefile-load: whole acl: acl_master - domain: 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa master: 225 zonefile-load: whole acl: acl_master {/collapse-item} {/collapse} After writing the configuration file, run the following command to restart KnotDNS: systemctl restart knot Editing Zone Files All configurations in this section are done on the primary DNS server. For record values (not hostnames) that require domain names, unless otherwise specified, please follow the RFC 1034 specification and use FQDN format. DN42 Domain Navigate to /var/lib/knot and create a file named <dn42_domain>.zone. SOA Record The first record of the zone must be the SOA record. The SOA record is the Start of Authority record, containing basic information about the domain, such as the primary NS server address. Fill in the following content: @ <TTL> SOA <Primary_NS_Server_Address> <Contact_Email> <Serial_Number> <Refresh_Time> <Retry_Time> <Expire_Time> <Minimum_TTL> @ represents the current domain itself, do not change it. TTL: The TTL (Time To Live) value for this SOA record. Primary NS Server Address: The address of the primary authoritative NS server for this domain. This can be a resolution within the domain. For example, my primary NS server is 172.20.234.225, and I plan to use ns1.yori.dn42. pointing to this address, so here I can fill in ns1.yori.dn42.. Contact Email: The email address, with @ replaced by .. For example, my email is i@iyoroy.cn, so here I can fill in i.iyoroy.cn. Serial Number: A 10-digit number following RFC 1912, representing the version of the zone file. Other DNS servers will refetch the records if they detect an increase in the serial number when querying the SOA. It's commonly encoded using the date + a sequence number, so this value should be incremented after each modification. Refresh Time:: The interval for AXFR slave nodes to pull the zone. Retry Time: The retry interval for AXFR slave nodes after a failed pull. Expire Time: The maximum time an AXFR slave node can continue serving with the last successfully pulled records after a failure, after which it stops responding. Minimum TTL: The minimum TTL value for the entire domain, the minimum refresh time for all records. Records won't be refreshed before at least this much time has passed. {collapse} {collapse-item label="Examp;e"} ; SOA @ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072705 60 60 1800 60 {/collapse-item} {/collapse} NS Records @ <TTL> NS <NS_Server_1> @ <TTL> NS <NS_Server_2> @ <TTL> NS <NS_Server_3> Fill this in according to your actual situation; add as many records as you have servers. {collapse} {collapse-item label="Example"} ; NS @ 3600 NS ns1.yori.dn42. @ 3600 NS ns2.yori.dn42. @ 3600 NS ns3.yori.dn42. {/collapse-item} {/collapse} A, AAAA, CNAME, etc. Records Fill them in according to the following format: <Hostname> <TTL> <Type> <Record_Value> If your NS server values point to hosts within your own DN42 domain, be sure to add A or AAAA resolution records for them. {collapse} {collapse-item label="Example"} ; A ns1 600 A 172.20.234.225 ns2 600 A 172.20.234.227 ns3 600 A 172.20.234.229 hkg-cn.node 600 A 172.20.234.225 nkg-cn.node 600 A 172.20.234.226 tyo-jp.node 600 A 172.20.234.227 hfe-cn.node 600 A 172.20.234.228 lax-us.node 600 A 172.20.234.229 ; AAAA ns1 600 AAAA fd18:3e15:61d0::1 ns2 600 AAAA fd18:3e15:61d0::3 ns3 600 AAAA fd18:3e15:61d0::5 hkg-cn.node 600 AAAA fd18:3e15:61d0::1 nkg-cn.node 600 AAAA fd18:3e15:61d0::2 tyo-jp.node 600 AAAA fd18:3e15:61d0::3 hfe-cn.node 600 AAAA fd18:3e15:61d0::4 lax-us.node 600 AAAA fd18:3e15:61d0::5 {/collapse-item} {collapse-item label="Complete Example"} /var/lib/knot/yori.dn42.zone ; SOA @ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072705 60 60 1800 60 ; NS @ 3600 NS ns1.yori.dn42. @ 3600 NS ns2.yori.dn42. @ 3600 NS ns3.yori.dn42. ; A ns1 600 A 172.20.234.225 ns2 600 A 172.20.234.227 ns3 600 A 172.20.234.229 hkg-cn.node 600 A 172.20.234.225 nkg-cn.node 600 A 172.20.234.226 tyo-jp.node 600 A 172.20.234.227 hfe-cn.node 600 A 172.20.234.228 lax-us.node 600 A 172.20.234.229 ; AAAA ns1 600 AAAA fd18:3e15:61d0::1 ns2 600 AAAA fd18:3e15:61d0::3 ns3 600 AAAA fd18:3e15:61d0::5 hkg-cn.node 600 AAAA fd18:3e15:61d0::1 nkg-cn.node 600 AAAA fd18:3e15:61d0::2 tyo-jp.node 600 AAAA fd18:3e15:61d0::3 hfe-cn.node 600 AAAA fd18:3e15:61d0::4 lax-us.node 600 AAAA fd18:3e15:61d0::5 {/collapse-item} {/collapse} IPv4 Reverse Lookup Domain Create a file in /var/lib/knot named <IPv4_Reverse_Lookup_Domain>.zone, replacing / with _. For example, my IPv4 block is 172.20.234.224/28, and my IPv4 reverse lookup domain is 224/28.234.20.172.in-addr.arpa, so the filename here would be 224_28.234.20.172.in-addr.arpa.zone. Fill in the resolution records: ; SOA @ <TTL> SOA <Primary_NS_Server_Address> <Contact_Email> <Serial_Number> <Refresh_Time> <Retry_Time> <Expire_Time> <Minimum_TTL> ; NS @ <TTL> NS <NS_Server_1> @ <TTL> NS <NS_Server_2> @ <TTL> NS <NS_Server_3> ; PTR <Last_IPv4_Octet> <TTL> PTR <Reverse_DNS_Value> <Last_IPv4_Octet> <TTL> PTR <Reverse_DNS_Value> <Last_IPv4_Octet> <TTL> PTR <Reverse_DNS_Value> ... The SOA and NS records are the same as above. Last IPv4 Octet: The last octet of the DN42 IPv4 address you assigned to the device. For example, my HK node is assigned 172.20.234.225, so here I would put 225. {collapse} {collapse-item label="Example"} 224_28.234.20.172.in-addr.arpa.zone ; SOA @ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072802 60 60 1800 60 ; NS @ 3600 NS ns1.yori.dn42. @ 3600 NS ns2.yori.dn42. @ 3600 NS ns3.yori.dn42. ; PTR 225 600 PTR hkg-cn.node.yori.dn42. 226 600 PTR nkg-cn.node.yori.dn42. 227 600 PTR tyo-jp.node.yori.dn42. 228 600 PTR hfe-cn.node.yori.dn42. 229 600 PTR lax-us.node.yori.dn42. {/collapse-item} {/collapse} You might wonder why the CIDR mask is needed, which differs from the common Clearnet format of reversed octets (e.g., 234.20.172.in-addr.arpa). Also, if you test locally, you might find that reverse lookups for your own IP addresses fail directly. The reason lies in DN42's distributed registry mechanism: a single zone file cannot cover all reverse query entry points for your address block (i.e., the .in-addr.arpa name for each specific IP address). To solve this, after your PR is merged, the official DN42 DNS will add CNAME redirects for your address block on its authoritative servers, pointing individual IP PTR queries to your CIDR-formatted zone, as shown below: ~$ dig PTR 225.234.20.172.in-addr.arpa +short 225.224/28.234.20.172.in-addr.arpa. # <-- CNAME added by Registry (redirect) hkg-cn.node.yori.dn42. # <-- Final PTR record returned by your DNS When an external resolver queries the reverse record for a specific IP (e.g., 172.20.234.225) (querying 225.234.20.172.in-addr.arpa), the official DN42 DNS returns a CNAME record pointing it to the specific record under the CIDR zone name (225.224/28.234.20.172.in-addr.arpa). Ultimately, the PTR record is provided by your configured authoritative DNS server. IPv6 Reverse Lookup Domain Create a file in /var/lib/knot named <IPv6_Reverse_Lookup_Domain>.zone. For example, my IPv6 block is fd18:3e15:61d0::/48, and my IPv6 reverse lookup domain is 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa, so the filename here would be 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa.zone. Fill in the resolution records: ; SOA @ <TTL> SOA <Primary_NS_Server_Address> <Contact_Email> <Serial_Number> <Refresh_Time> <Retry_Time> <Expire_Time> <Minimum_TTL> ; NS @ <TTL> NS <NS_Server_1> @ <TTL> NS <NS_Server_2> @ <TTL> NS <NS_Server_3> ; PTR <Reversed_Last_20_Nibbles> <TTL> PTR <Reverse_DNS_Value> <Reversed_Last_20_Nibbles> <TTL> PTR <Reverse_DNS_Value> <Reversed_Last_20_Nibbles> <TTL> PTR <Reverse_DNS_Value> ... Handle SOA and NS records as above. For the PTR hostname, you need to take the last 80 bits of the host's IPv6 address (after removing the /48 prefix), expand them into 20 hexadecimal characters, and reverse the order of these characters, separating them with dots. For example, my Hong Kong node's IPv6 is fd18:3e15:61d0::1. Expanded, this is fd18:3e15:61d0:0000:0000:0000:0000:0001. The hostname here would be 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0. {collapse} {collapse-item label="Example"} 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa.zone ; SOA @ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072802 60 60 1800 60 ; NS @ 3600 NS ns1.yori.dn42. @ 3600 NS ns2.yori.dn42. @ 3600 NS ns3.yori.dn42. ; PTR 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR hkg-cn.node.yori.dn42. 2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR nkg-cn.node.yori.dn42. 3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR tyo-jp.node.yori.dn42. 4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR hfe-cn.node.yori.dn42. 5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR lax-us.node.yori.dn42. {/collapse-item} {/collapse} Verifying the Setup After saving everything, run knot reload on each DNS server. If all goes well, you should see the slave nodes synchronizing the zone files from the master node. You can then use dig or nslookup specifying the server to query the resolution records. Registration Domain Clone the DN42 Registry, navigate to data/dns, create a new file named <your_desired_domain>, and fill in the following content: domain: <your_desired_domain> admin-c: <Admin_NIC_Handle> tech-c: <Tech_NIC_Handle> mnt-by: <Maintainer> nserver: <NS1_Server_Domain> <NS1_Server_IP> nserver: <NS2_Server_Domain> <NS2_Server_IP> nserver: <NS3_Server_Domain> <NS3_Server_IP> ... source: DN42 Refer to DN42 - Ep.1 Joining the DN42 Network for admin-c, tech-c, and mnt-by. {collapse} {collapse-item label="Example"} data/dns/yori.dn42 domain: yori.dn42 admin-c: IYOROY-DN42 tech-c: IYOROY-DN42 mnt-by: IYOROY-MNT nserver: ns1.yori.dn42 172.20.234.225 nserver: ns1.yori.dn42 fd18:3e15:61d0::1 nserver: ns2.yori.dn42 172.20.234.227 nserver: ns2.yori.dn42 fd18:3e15:61d0::3 nserver: ns3.yori.dn42 172.20.234.229 nserver: ns3.yori.dn42 fd18:3e15:61d0::5 source: DN42 {/collapse-item} {/collapse} IPv4 Reverse Lookup Domain Navigate to data/inetnum, find the file for your registered address block, and add nserver fields pointing to your own DNS servers: nserver: <your_DNS_server_address> nserver: <your_DNS_server_address> ... ... {collapse} {collapse-item label="Example"} diff --git a/data/inetnum/172.20.234.224_28 b/data/inetnum/172.20.234.224_28 index 50c800945..5ad60e23d 100644 --- a/data/inetnum/172.20.234.224_28 +++ b/data/inetnum/172.20.234.224_28 @@ -8,3 +8,6 @@ tech-c: IYOROY-DN42 mnt-by: IYOROY-MNT status: ASSIGNED source: DN42 +nserver: ns1.yori.dn42 +nserver: ns2.yori.dn42 +nserver: ns3.yori.dn42 {/collapse-item} {/collapse} IPv6 Reverse Lookup Domain Navigate to data/inet6num, find the file for your registered address block, and add nserver fields pointing to your own DNS servers: nserver: <your_DNS_server_address> nserver: <your_DNS_server_address> ... {collapse} {collapse-item label="Example"} diff --git a/data/inet6num/fd18:3e15:61d0::_48 b/data/inet6num/fd18:3e15:61d0::_48 index 53f0de06d..1ae067b00 100644 --- a/data/inet6num/fd18:3e15:61d0::_48 +++ b/data/inet6num/fd18:3e15:61d0::_48 @@ -8,3 +8,6 @@ tech-c: IYOROY-DN42 mnt-by: IYOROY-MNT status: ASSIGNED source: DN42 +nserver: ns1.yori.dn42 +nserver: ns2.yori.dn42 +nserver: ns3.yori.dn42 {/collapse-item} {/collapse} Submit a PR and Wait for Merge After filling everything out, push your changes and submit a Pull Request. Because anyone in DN42 can run recursive DNS, it might take up to a week for the DNS configuration to fully propagate, although I found that the public DNS (172.20.0.53) could query my records within half a day after merging. Special thanks to たのしい for clarifying the differences between IPv4 reverse lookup in DN42 and the public internet. Reference Articles: https://www.haiyun.me/archives/1398.html https://www.jianshu.com/p/7d69ec2976c7 https://www.potat0.cc/posts/20220726/Register_DN42_Domain/ https://bbs.csdn.net/topics/393775423 https://blog.snorlax.blue/knot-reverse-dns-kickstart/ http://www.kkdlabs.jp/dns/automatic-dnssec-signing-by-knot-dns/ https://lantian.pub/article/modify-website/register-own-domain-in-dn42.lantian/ https://datatracker.ietf.org/doc/html/rfc2317 https://datatracker.ietf.org/doc/html/rfc3152 https://datatracker.ietf.org/doc/html/rfc1912#section-2.2
DN42 - Ep.2 Building Internal Network with OSPF and Enabling iBGP
Foreword I am a novice in BGP. This article may contain imprecise content/naive understandings/elementary mistakes. I kindly ask the experts to be lenient. If you find any issues, you are welcome to contact me via email, and I will correct them as soon as possible. If you find this unacceptable, it is recommended to close this article now. Article Update Log {timeline} {timeline-item color="#50BFFF"} July 22, 2025: First edition published, using VXLAN over WireGuard tunnel. {/timeline-item} {timeline-item color="#50BFFF"} July 25, 2025: Updated tunneling solution, using type ptp; to support OSPF traffic via WireGuard (Special thanks to Nuro Trance for the guidance!). {/timeline-item} {timeline-item color="#50BFFF"} August 8, 2025: Added explanation and configuration for iBGP. {/timeline-item} {timeline-item color="#4F9E28"} August 27, 2025: Updated node topology diagram. {/timeline-item} {/timeline} Why Do We Need Internal Routing? As the number of nodes increases, we need a proper way to handle internal routing within our AS (Autonomous System). BGP only handles routing to different ASes, which causes a problem: if nodes A and B are both peering with external networks, a request from node A may have its response routed to node B, even though they are part of the same AS. Without internal routing, node A will not receive the reply. To solve this, we need to ensure that all devices within our AS can communicate with each other. The common solutions are: Using network tools like ZeroTier: Simple to set up, just install the client on each node for P2P connectivity. Using P2P tools like WireGuard to manually create $\frac{n(n-1)}{2}$ tunnels, which works like the first solution but becomes cumbersome as nodes grow. Using WireGuard to establish $\frac{n(n-1)}{2}$ tunnels, then using an internal routing protocol like OSPF or Babel to manage the routing. This is more flexible and easier to scale, but it can be risky and could break the DN42 network due to misconfigurations. Thus, I decided to take the risk. Node Topology graph LR A[HKG 172.20.234.225 fd18:3e15:61d0::1] B[NKG 172.20.234.226 fd18:3e15:61d0::2] C[TYO 172.20.234.227 fd18:3e15:61d0::3] D[FRA 172.20.234.228 fd18:3e15:61d0::4] E[LAX 172.20.234.229 fd18:3e15:61d0::5] B <--> A C <--> A A <--> E A <--> D C <--> D C <--> E D <--> E Update Bird2 to v2.16 or Above To use IPv6 Link-Local addresses to transmit IPv4 OSPF data, Bird v2.16 or later is required. Here are the steps to update: sudo apt update && sudo apt -y install apt-transport-https ca-certificates wget lsb-release sudo wget -O /usr/share/keyrings/cznic-labs-pkg.gpg https://pkg.labs.nic.cz/gpg echo "deb [signed-by=/usr/share/keyrings/cznic-labs-pkg.gpg] https://pkg.labs.nic.cz/bird2 $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/cznic-labs-bird2.list sudo apt update && sudo apt install bird2 -y Tunnel Configuration [Interface] PrivateKey = <Local WireGuard Private Key> ListenPort = <Listen Port> Table = off Address = <IPv6 LLA>/64 PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0 [Peer] PublicKey = <Peer Public Key> Endpoint = <Peer Public Endpoint> AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8, ff02::5 ff02::5is the OSPFv3 router-specific link-local multicast address and should be included in AllowedIPs. If you're using Bird versions earlier than v2.16, you'll need to add an IPv4 address for the tunnel as well. See the example below: {collapse} {collapse-item label="WireGuard Configuration Example with IPv4"} [Interface] PrivateKey = <Local WireGuard Private Key> ListenPort = <Listen Port> Table = off Address = <IPv6 LLA>/64 PostUp = ip addr add 100.64.0.225/32 peer 100.64.0.226/32 dev %i PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0 [Peer] PublicKey = <Peer Public Key> Endpoint = <Peer Public Endpoint> AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 100.64.0.0/16, 172.31.0.0/16, fd00::/8, fe00::/8, ff02::5 Please replace 100.64.0.225 and 100.64.0.226 with your local and peer IPv4 addresses, and remember to add AllowedIPs. {/collapse-item} {/collapse} Enable OSPF You should have already configured basic Bird settings as described in the previous article. Create a new file called ospf.conf under /etc/bird and add the following: protocol ospf v3 <name> { ipv4 { import where is_self_net() && source != RTS_BGP; export where is_self_net() && source != RTS_BGP; }; include "/etc/bird/ospf/*"; }; protocol ospf v3 <name> { ipv6 { import where is_self_net_v6() && source != RTS_BGP; export where is_self_net_v6() && source != RTS_BGP; }; include "/etc/bird/ospf/*"; }; Theoretically, OSPF v2 should be used for handling IPv4, but since we need to communicate IPv4 using IPv6 Link-Local addresses, we are using OSPF v3 for IPv4 in this case as well. The filter rules ensure that only routes within the local network segment are allowed to propagate through OSPF, and routes from external BGP protocols are filtered out. Never use import all; export all; indiscriminately, as this could lead to route hijacking and affect the entire DN42 network. OSPF should only handle internal network routes. {collapse} {collapse-item label="Example"} /etc/bird/ospf.conf protocol ospf v3 dn42_iyoroynet_ospf { ipv4 { import where is_self_net() && source != RTS_BGP; export where is_self_net() && source != RTS_BGP; }; include "/etc/bird/ospf/*"; }; protocol ospf v3 dn42_iyoroynet_ospf6 { ipv6 { import where is_self_net_v6() && source != RTS_BGP; export where is_self_net_v6() && source != RTS_BGP; }; include "/etc/bird/ospf/*"; }; {/collapse-item} {/collapse} Next, create the /etc/bird/ospf folder and then create an area configuration file (e.g., /etc/bird/ospf/backbone.conf) with the following content: area 0.0.0.0 { interface "<DN42 dummy interface>" { stub; }; interface "<wg0 interface>" { cost 80; # Modify according to your network situation type ptp; }; interface "<wg1 interface>" { cost 100; # Modify according to your network situation type ptp; }; # Continue for other interfaces }; The 0.0.0.0 area represents the backbone network. he dummy interface here refers to the DN42 virtual interface mentioned in the previous article The cost value is typically used for cost calculation but in DN42's case, where bandwidth is less critical but latency is more important, you can directly assign the latency value. OSPF will automatically choose the route with the lowest cost (sum of the cost values). {collapse} {collapse-item label="Example"} /etc/bird/ospf/backbone.conf area 0.0.0.0 { interface "dn42" { stub; }; interface "dn42_hkg" { cost 80; type ptp; }; interface "dn42_hfe" { cost 150; type ptp; }; interface "dn42_lax"{ cost 100; type ptp; }; }; {/collapse-item} {/collapse} Finally, open /etc/bird/bird.conf and add the following to include the OSPF configuration file at the end: include "ospf.conf"; Run birdc configure, and then birdc show protocols should show the OSPF status as Running. If not, check the configuration steps for errors. At this point, you should be able to ping between two non-directly connected machines: Enable iBGP Before establishing multiple peer connections, each of your nodes must first have complete knowledge of the internal AS topology. This involves configuring another key component: internal BGP (iBGP). Necessity of iBGP iBGP ensures that all routers within the AS have complete knowledge of external destination routes. It ensures that: Internal routers can select the best exit path. Traffic is correctly routed to the boundary routers responsible for specific external networks. Even if there are multiple boundary routers connected to the same external network, internal routers can choose the best exit based on policies. Compared to using a default route pointing to the border router within the AS, iBGP provides precise external route information, allowing internal routers to make more intelligent forwarding decisions. Disadvantages and Solutions To prevent uncontrolled propagation of routing information within the AS, which could cause loops, an iBGP router will not readvertise routes learned from one iBGP neighbor to other iBGP neighbors. This necessitates that traditional iBGP requires a full mesh of iBGP neighbor relationships between all iBGP-running routers within the same AS. (You still need to establish $\frac{n(n+1)}{2}$ connections , there's no way around it. But configuring iBGP is still easier than configuring tunnels after OSPF is set up ). Solutions include: Using a Route Reflector (RR): An RR router manages all routing information within the entire AS. The disadvantage is that if the RR router fails, the entire network can be paralyzed (which is not very Decentralized). Using BGP Confederation: This involves virtually dividing the routers within the AS into sub-ASes, treating the connections between routers as eBGP, and finally stripping the internal AS path information when advertising routes externally. I haven't tried the latter two solutions. Here are some potentially useful reference articles. This article focuses on the configuration of iBGP. DN42 Experimental Network: Intro and Registration (Updated 2022-12) - Lan Tian @ Blog Configure BGP Confederation & Fake Confederation in Bird (Updated 2020-06-07) - Lan Tian @ Blog Writing the iBGP Configuration File Create a new file ibgp.conf in /etc/bird and fill it with the following content: template bgp ibgpeers { local as OWNAS; ipv4 { import where source = RTS_BGP && is_valid_network() && !is_self_net(); export where source = RTS_BGP && is_valid_network() && !is_self_net(); next hop self; extended next hop; }; ipv6 { import where source = RTS_BGP && is_valid_network_v6() && !is_self_net_v6(); export where source = RTS_BGP && is_valid_network_v6() && !is_self_net_v6(); next hop self; }; }; include "ibgp/*"; The import and export filters ensure that iBGP only processes routes learned via the BGP protocol and filters out IGP routes to prevent loops. next hop self is required. It instructs BIRD to rewrite the next hop to the border router's own IP address (instead of the original external next hop) when exporting routes to iBGP neighbors. This is because internal routers cannot directly access the external neighbor's address; without rewriting, the address would be considered unreachable. After rewriting, internal routers only need to send traffic to the border router via IGP routing, and the border router handles the final external forwarding. Because I want to use IPv6 addresses to establish MP-BGP and route IPv4 over IPv6, extended next hop is enabled for IPv4. Next, create the /etc/bird/ibgp directory. Inside, create an iBGP Peer configuration file for each node: protocol bgp 'dn42_ibgp_<Node Name>' from ibgpeers{ neighbor <Corresponding Node's IPv6 ULA Address> as OWNAS; }; {collapse} {collapse-item label="Example"} /etc/bird/ibgp/hkg.conf: protocol bgp 'dn42_ibgp_HKG' from ibgpeers{ neighbor fd18:3e15:61d0::1 as OWNAS; }; {/collapse-item} {/collapse} Note: Each node needs to establish (n-1) iBGP connections, ensuring connectivity with all other machines within the AS. This is why ULA addresses are used. Using ULA addresses ensures that even if the WireGuard connection between two nodes goes down, iBGP can still establish connections via the internal routing established by OSPF. Otherwise, it could lead to the collapse of the entire internal network. Finally, add the inclusion of ibgp.conf in /etc/bird/bird.conf: include "ibgp.conf"; And run birdc configure to apply the configuration. References: BIRD 与 BGP 的新手开场 - 海上的宫殿 萌新入坑 DN42 之 —— 基于 tailscale + vxlan + OSPF 的组网 – 米露小窝 使用 Bird2 配置 WireGuard + OSPF 实现网络的高可用 | bs' realm DN42 实验网络介绍及注册教程（2022-12 更新） - Lan Tian @ Blog 如何引爆 DN42 网络（2023-05-12 更新） - Lan Tian @ Blog Bird 配置 BGP Confederation，及模拟 Confederation（2020-06-07 更新） - Lan Tian @ Blog 深入解析OSPF路径开销、优先级和计时器 - 51CTO New release 2.16 | BIRD Internet Routing Daemon 第一章·第二节 如何在 Linux 上安装最新版本的 BIRD？ | BIRD 中文文档 [DN42] 使用 OSPF ptp 搭建内网与IBGP配置 – Xe_iu's Blog | Xe_iu的杂物间 [译] dn42 多服务器环境中的 iBGP 与 IGP 配置 | liuzhen932 的小窝