Compare commits
	
		
			4 Commits
		
	
	
		
			a2d5333f01
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 226fe6577f | |||
| 376aa0c9b6 | |||
| 2f5f746492 | |||
| 01a77e65a6 | 
							
								
								
									
										143
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										143
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,34 +1,125 @@ | ||||
| # sixnix | ||||
|  | ||||
| My ISP provides me a single `/64` and it's dynamically assigned. I cannot have multiple networks with functional autoconfiguration. One solution to this is use one of 4to6 [tunnel brokers](https://tunnelbroker.services/) like the one offered by [Hurricane Electric](https://tunnelbroker.net/). Since my ISP uses CGNAT this setup is tricky if not impossible. | ||||
| IPv6 WireGuard tunnels with optional BGP failover. | ||||
|  | ||||
| Fortunately Linode provides free `/56` to customers and the cheapest VPS costs just $5/month. You get 256 `/64` subnets. The following table provides an example where you can have 15 delegations of `/60` and use the 16th `/60` for the wireguard peer addressing. | ||||
| ## Motivation | ||||
|  | ||||
| My ISP gives me a single `/64` and it's dynamically assigned. Can't have multiple networks with functional autoconfiguration. Tunnel brokers like [Hurricane Electric](https://tunnelbroker.net/) would work but my ISP uses CGNAT which makes that setup tricky if not impossible. They also do not have any exit nodes in the region I want. | ||||
|  | ||||
| Linode provides a routed `/56` and 1TB traffic at 1 Gbit/s for with their cheapest $5/month VPS. That's 256 `/64` subnets. | ||||
|  | ||||
| ## Solution 1: Simple WireGuard tunnels | ||||
|  | ||||
| Just a plain WireGuard tunnel to route the `/56` to your home network. | ||||
|  | ||||
| With a `/56` you can carve out a `/60` for WireGuard endpoints and you're left with 15 `/60` subnets. Share it with friends! | ||||
|  | ||||
| | Client subnet      | WireGuard subnet     |    | | ||||
| |--------------------|---------------------|----| | ||||
| | 2001:db8::/60      | 2001:db8:0:f0::/64  | 1  | | ||||
| | 2001:db8:0:10::/60 | 2001:db8:0:f1::/64  | 2  | | ||||
| | 2001:db8:0:20::/60 | 2001:db8:0:f2::/64  | 3  | | ||||
| | 2001:db8:0:30::/60 | 2001:db8:0:f3::/64  | 4  | | ||||
| | 2001:db8:0:40::/60 | 2001:db8:0:f4::/64  | 5  | | ||||
| | 2001:db8:0:50::/60 | 2001:db8:0:f5::/64  | 6  | | ||||
| | 2001:db8:0:60::/60 | 2001:db8:0:f6::/64  | 7  | | ||||
| | 2001:db8:0:70::/60 | 2001:db8:0:f7::/64  | 8  | | ||||
| | 2001:db8:0:80::/60 | 2001:db8:0:f8::/64  | 9  | | ||||
| | 2001:db8:0:90::/60 | 2001:db8:0:f9::/64  | 10 | | ||||
| | 2001:db8:0:a0::/60 | 2001:db8:0:fa::/64  | 11 | | ||||
| | 2001:db8:0:b0::/60 | 2001:db8:0:fb::/64  | 12 | | ||||
| | 2001:db8:0:c0::/60 | 2001:db8:0:fc::/64  | 13 | | ||||
| | 2001:db8:0:d0::/60 | 2001:db8:0:fd::/64  | 14 | | ||||
| | 2001:db8:0:e0::/60 | 2001:db8:0:fe::/64  | 15 | | ||||
| |--------------------|----------------------|----| | ||||
| | 2001:db8::/60      | 2001:db8:0:f0::a/127 | 1  | | ||||
| | 2001:db8:0:10::/60 | 2001:db8:0:f1::a/127 | 2  | | ||||
| | 2001:db8:0:20::/60 | 2001:db8:0:f2::a/127 | 3  | | ||||
| | 2001:db8:0:30::/60 | 2001:db8:0:f3::a/127 | 4  | | ||||
| | 2001:db8:0:40::/60 | 2001:db8:0:f4::a/127 | 5  | | ||||
| | 2001:db8:0:50::/60 | 2001:db8:0:f5::a/127 | 6  | | ||||
| | 2001:db8:0:60::/60 | 2001:db8:0:f6::a/127 | 7  | | ||||
| | 2001:db8:0:70::/60 | 2001:db8:0:f7::a/127 | 8  | | ||||
| | 2001:db8:0:80::/60 | 2001:db8:0:f8::a/127 | 9  | | ||||
| | 2001:db8:0:90::/60 | 2001:db8:0:f9::a/127 | 10 | | ||||
| | 2001:db8:0:a0::/60 | 2001:db8:0:fa::a/127 | 11 | | ||||
| | 2001:db8:0:b0::/60 | 2001:db8:0:fb::a/127 | 12 | | ||||
| | 2001:db8:0:c0::/60 | 2001:db8:0:fc::a/127 | 13 | | ||||
| | 2001:db8:0:d0::/60 | 2001:db8:0:fd::a/127 | 14 | | ||||
| | 2001:db8:0:e0::/60 | 2001:db8:0:fe::a/127 | 15 | | ||||
|  | ||||
| | Host                  | Subnet                      | | ||||
| |-----------------------|-----------------------------| | ||||
| | wireguard.example.com | 2001:0db8:0:ff::/64         | | ||||
| ### Basic configuration | ||||
|  | ||||
| You can follow the linode's [nixos installation guide](https://www.linode.com/docs/guides/install-nixos-on-linode/) and/or use this repo as a reference to set up your wireguard tunnels. | ||||
| ```nix | ||||
| { | ||||
|   inputs.sixnix.url = "git+https://code.planet-express.in/konarak/sixnix.git"; | ||||
|  | ||||
| - set up sops using the [docs here](https://github.com/Mic92/sops-nix) | ||||
| - add root password, wireguard server private keys and peer public keys | ||||
| - add ssh public keys and configure any additional user accounts in `modules/access.nix` | ||||
| - configure egress, firewall, wireguard tunnels etc in `modules/network.nix` | ||||
|   outputs = { nixpkgs, sixnix, ... }: { | ||||
|     nixosConfigurations.tunnel-server = nixpkgs.lib.nixosSystem { | ||||
|       modules = [ | ||||
|         sixnix.nixosModules.default | ||||
|         { | ||||
|           sixnix = { | ||||
|             enable = true; | ||||
|             hostname = "tunnel-server"; | ||||
|             domain = "example.com"; | ||||
|           }; | ||||
|  | ||||
|           wireguard.interfaces = [{ | ||||
|             interface = "wg0"; | ||||
|             serverPort = 51820; | ||||
|             serverAddress = "2001:db8:a7c4:0:f0::a/127"; | ||||
|             clientAddress = "2001:db8:a7c4:0:f0::b/127"; | ||||
|             clientSubnet = "2001:db8:a7c4::/60"; | ||||
|             serverPrivateKeyFile = "/run/secrets/wg0-server-private"; | ||||
|             clientPublicKeyFile = "/run/secrets/wg0-client-public"; | ||||
|           }]; | ||||
|         } | ||||
|       ]; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| How it works: | ||||
| - Your router uses the ISP-provided IPv6 to connect to the WireGuard server | ||||
| - Interfaces get `/64`s from the delegated `/60` with router advertisement enabled | ||||
| - All traffic from clients goes out to the internet via Linode | ||||
|  | ||||
| ## Solution 2: WireGuard tunnels with BGP for failover | ||||
|  | ||||
| Want redundancy? Run two Linodes in the same datacenter and use BGP to share a single `/56` between them. If one server goes down, BGP automatically fails over to the other. | ||||
|  | ||||
| Cost: $10/month + tax for two Linodes with pooled 2TB traffic quota. | ||||
|  | ||||
| ### BGP configuration | ||||
|  | ||||
| ```nix | ||||
| { | ||||
|   sixnix = { | ||||
|     enable = true; | ||||
|     hostname = "tunnel-primary"; | ||||
|     domain = "example.com"; | ||||
|  | ||||
|     bgp = { | ||||
|       enable = true; | ||||
|       peers = [ | ||||
|         "2001:db8:9f2b::1"  # BGP peer addresses (e.g., Linode route servers) | ||||
|         "2001:db8:9f2b::2" | ||||
|         "2001:db8:9f2b::3" | ||||
|         "2001:db8:9f2b::4" | ||||
|       ]; | ||||
|       advertisedSubnets = [ "2001:db8:a7c4::/56" ]; | ||||
|       routeMap = "primary";  # or "secondary" for the backup server | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   wireguard.interfaces = [{ | ||||
|     interface = "wg0"; | ||||
|     serverPort = 51820; | ||||
|     serverAddress = "2001:db8:a7c4:0:f0::a/127"; | ||||
|     clientAddress = "2001:db8:a7c4:0:f0::b/127"; | ||||
|     clientSubnet = "2001:db8:a7c4::/60"; | ||||
|     serverPrivateKeyFile = "/run/secrets/wg0-server-private"; | ||||
|     clientPublicKeyFile = "/run/secrets/wg0-client-public"; | ||||
|   }]; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| The BGP setup creates: | ||||
| - A shared dummy interface with a VIP that both servers announce | ||||
| - Route servers configured automatically based on datacenter ID | ||||
| - Blackhole routes for the advertised subnet | ||||
| - Primary/secondary route maps for failover priority | ||||
|  | ||||
| ## Notes | ||||
|  | ||||
| - BGP peers and advertised subnets are fully configurable | ||||
| - The FRR configuration template may need adjustments for providers other than Linode (AS numbers, communities, etc.) | ||||
| - For Linode: use route servers `2600:3c0f:<dcId>:34::{1,2,3,4}` where `<dcId>` is your datacenter ID (e.g., 25 for in-maa) | ||||
| - See Linode's [BGP documentation](https://techdocs.akamai.com/cloud-computing/docs/configuring-ip-failover-over-bgp-using-frr-advanced) for more details | ||||
| - See [`modules/network.nix`](modules/network.nix) and [`modules/wireguard.nix`](modules/wireguard.nix) for all available options | ||||
| @@ -1,45 +0,0 @@ | ||||
| { config, lib, pkgs, ... }: | ||||
|  | ||||
| { | ||||
|   sops.defaultSopsFile = ../secrets/secrets.yaml; | ||||
|  | ||||
|   imports = [ | ||||
|     # Include the results of the hardware scan. | ||||
|     ./hardware-configuration.nix | ||||
|  | ||||
|     ../modules/sixnix | ||||
|   ]; | ||||
|  | ||||
|   # Use the GRUB 2 boot loader. | ||||
|   boot.loader.grub.enable = true; | ||||
|  | ||||
|   # Enable the Flakes feature and the accompanying new nix command-line tool | ||||
|   nix.settings.experimental-features = [ "nix-command" "flakes" ]; | ||||
|  | ||||
|   environment.systemPackages = with pkgs; [ git curl btop emacs-nox ]; | ||||
|  | ||||
|   # Copy the NixOS configuration file and link it from the resulting system | ||||
|   # (/run/current-system/configuration.nix). This is useful in case you | ||||
|   # accidentally delete configuration.nix. | ||||
|   # system.copySystemConfiguration = true; | ||||
|  | ||||
|   # This option defines the first version of NixOS you have installed on this particular machine, | ||||
|   # and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions. | ||||
|   # | ||||
|   # Most users should NEVER change this value after the initial install, for any reason, | ||||
|   # even if you've upgraded your system to a new NixOS release. | ||||
|   # | ||||
|   # This value does NOT affect the Nixpkgs version your packages and OS are pulled from, | ||||
|   # so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how | ||||
|   # to actually do that. | ||||
|   # | ||||
|   # This value being lower than the current NixOS release does NOT mean your system is | ||||
|   # out of date, out of support, or vulnerable. | ||||
|   # | ||||
|   # Do NOT change this value unless you have manually inspected all the changes it would make to your configuration, | ||||
|   # and migrated your data accordingly. | ||||
|   # | ||||
|   # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion . | ||||
|   system.stateVersion = "25.05"; # Did you read the comment? | ||||
|  | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| # Do not modify this file!  It was generated by ‘nixos-generate-config’ | ||||
| # and may be overwritten by future invocations.  Please make changes | ||||
| # to /etc/nixos/configuration.nix instead. | ||||
| { config, lib, pkgs, modulesPath, ... }: | ||||
|  | ||||
| { | ||||
|   imports = [ (modulesPath + "/profiles/qemu-guest.nix") ]; | ||||
|  | ||||
|   boot.initrd.availableKernelModules = | ||||
|     [ "virtio_pci" "virtio_scsi" "ahci" "sd_mod" ]; | ||||
|   boot.initrd.kernelModules = [ ]; | ||||
|   boot.kernelModules = [ "wireguard" ]; | ||||
|   boot.extraModulePackages = [ ]; | ||||
|  | ||||
|   fileSystems."/" = { | ||||
|     device = "/dev/sda"; | ||||
|     fsType = "ext4"; | ||||
|   }; | ||||
|  | ||||
|   swapDevices = [ ]; | ||||
|  | ||||
|   # Enables DHCP on each ethernet and wireless interface. In case of scripted networking | ||||
|   # (the default) this is the recommended approach. When using systemd-networkd it's | ||||
|   # still possible to use this option, but it's recommended to use it in conjunction | ||||
|   # with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`. | ||||
|   # networking.useDHCP = lib.mkDefault true; | ||||
|   # networking.interfaces.enp0s4.useDHCP = lib.mkDefault true; | ||||
|  | ||||
|   nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; | ||||
|  | ||||
|   boot.kernelParams = [ "console=ttyS0,19200n8" ]; | ||||
|   boot.loader.grub.extraConfig = '' | ||||
|     serial --speed=19200 --unit=0 --word=8 --parity=no --stop=1; | ||||
|     terminal_input serial; | ||||
|     terminal_output serial; | ||||
|   ''; | ||||
|   boot.loader.grub.forceInstall = true; | ||||
|   boot.loader.grub.device = "nodev"; | ||||
|   boot.loader.timeout = 10; | ||||
|  | ||||
| } | ||||
							
								
								
									
										23
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								flake.nix
									
									
									
									
									
								
							| @@ -1,25 +1,12 @@ | ||||
| { | ||||
|   description = "Server Configuration for WireGuard provider"; | ||||
|   description = "Simple WireGuard tunnelbroker, with optional failover using BGP"; | ||||
|  | ||||
|   inputs = { | ||||
|     nixpkgs.url = "nixpkgs/nixos-25.05"; | ||||
|     nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; | ||||
|  | ||||
|     sops-nix.url = "github:Mic92/sops-nix"; | ||||
|     sops-nix.inputs.nixpkgs.follows = "nixpkgs"; | ||||
|  | ||||
|     nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; | ||||
|   }; | ||||
|  | ||||
|   outputs = { self, nixpkgs, nixpkgs-unstable, sops-nix, }@attrs: { | ||||
|     nixosConfigurations = { | ||||
|       "tunnels" = nixpkgs.lib.nixosSystem { | ||||
|         system = "x86_64-linux"; | ||||
|         specialArgs = attrs; | ||||
|         modules = [ | ||||
|           (import ./config/maa1-ops/configuration.nix) | ||||
|           sops-nix.nixosModules.sops | ||||
|         ]; | ||||
|       }; | ||||
|     }; | ||||
|   outputs = { self, nixpkgs }: { | ||||
|     nixosModules.default = import ./modules; | ||||
|     nixosModules.sixnix = import ./modules; | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -1,44 +0,0 @@ | ||||
| { config, pkgs, ... }: | ||||
|  | ||||
| let | ||||
|   keys = [ | ||||
|   ]; | ||||
|  | ||||
|   root.password = config.sops.secrets.password-root.path; | ||||
| in { | ||||
|  | ||||
|   sops.secrets = let def = { neededForUsers = true; }; | ||||
|   in { | ||||
|     "password-root" = def; | ||||
|   }; | ||||
|  | ||||
|   services.openssh = { | ||||
|     enable = true; | ||||
|     settings.PermitRootLogin = "no"; | ||||
|   }; | ||||
|  | ||||
|   # protect from ssh spammers | ||||
|   services.sshguard.enable = true; | ||||
|  | ||||
|   # disable kernel from logging REFUSED CONNECTIONS messages when we actually drop this traffic | ||||
|   networking.firewall.logRefusedConnections = false; | ||||
|  | ||||
|   # enable mosh and open firewall ports | ||||
|   programs.mosh.enable = true; | ||||
|  | ||||
|   security.sudo.wheelNeedsPassword = false; | ||||
|  | ||||
|   users.mutableUsers = false; | ||||
|  | ||||
|   users.users.root = { | ||||
|     hashedPasswordFile = root.password; | ||||
|     openssh.authorizedKeys.keys = keys; | ||||
|   }; | ||||
|  | ||||
|   # users.users.<username> = { | ||||
|   #   isNormalUser = true; | ||||
|   #   extraGroups = [ "wheel" ]; | ||||
|   #   hashedPasswordFile = <username.password>; | ||||
|   #   openssh.authorizedKeys.keys = <username.keys>; | ||||
|   # }; | ||||
| } | ||||
| @@ -1 +1 @@ | ||||
| { config, pkgs, ... }: { imports = [ ./access.nix ./network.nix ]; } | ||||
| { config, pkgs, ... }: { imports = [ ./network.nix ./wireguard.nix ]; } | ||||
|   | ||||
| @@ -1,33 +1,105 @@ | ||||
| { config, pkgs, ... }: | ||||
| let | ||||
|   hostname = "tunnels"; | ||||
|   domain = "example.com"; | ||||
| { config, pkgs, lib, ... }: | ||||
|  | ||||
|   resolvers = [ "9.9.9.9" "2620:fe::fe" ]; | ||||
| with lib; | ||||
|  | ||||
|   egress = { | ||||
|     interface = "eth0"; | ||||
| let cfg = config.sixnix; | ||||
| in { | ||||
|   options.sixnix = { | ||||
|     enable = mkEnableOption "sixnix networking configuration"; | ||||
|  | ||||
|     ipv4.gateway = "198.51.100.10"; | ||||
|     ipv4.address = "198.51.100.1/24"; | ||||
|  | ||||
|     ipv6.gateway = "fe80::1"; | ||||
|     ipv6.address = "2001:DB8:10:cafe:dcaf::3210/64"; | ||||
|     hostname = mkOption { | ||||
|       type = types.str; | ||||
|       description = "Server hostname"; | ||||
|     }; | ||||
|  | ||||
|   tunnels = [{ | ||||
|     interface = "wg0"; | ||||
|     serverPort = 51820; | ||||
|     serverAddress = "2001:db8:de:ff0::1/128"; | ||||
|     clientAddress = "2001:db8:de:ff0::2/128"; | ||||
|     clientSubnet = "2001:db8:de:f00::/60"; | ||||
|     serverPrivateKeyFile = "wg0-server-private"; | ||||
|     clientPublicKeyFile = "wg0-client-public"; | ||||
|   }]; | ||||
| in { | ||||
|     domain = mkOption { | ||||
|       type = types.str; | ||||
|       default = "notwork.in"; | ||||
|       description = "Domain name"; | ||||
|     }; | ||||
|  | ||||
|   networking.hostName = hostname; | ||||
|   networking.domain = domain; | ||||
|     interface = mkOption { | ||||
|       type = types.str; | ||||
|       default = "enp0s3"; | ||||
|       description = "Network interface name"; | ||||
|     }; | ||||
|  | ||||
|     dns = mkOption { | ||||
|       type = types.listOf types.str; | ||||
|       default = [ "9.9.9.9" "2620:fe::fe" ]; | ||||
|       description = "DNS resolvers for systemd-networkd"; | ||||
|     }; | ||||
|  | ||||
|     ipv4 = { | ||||
|       address = mkOption { | ||||
|         type = types.nullOr types.str; | ||||
|         default = null; | ||||
|         description = "IPv4 address with CIDR (documentation only - using DHCP)"; | ||||
|       }; | ||||
|  | ||||
|       gateway = mkOption { | ||||
|         type = types.nullOr types.str; | ||||
|         default = null; | ||||
|         description = "IPv4 gateway (documentation only - using DHCP)"; | ||||
|       }; | ||||
|     }; | ||||
|  | ||||
|     ipv6 = { | ||||
|       address = mkOption { | ||||
|         type = types.nullOr types.str; | ||||
|         default = null; | ||||
|         description = "IPv6 address with CIDR (documentation only - using SLAAC)"; | ||||
|       }; | ||||
|  | ||||
|       gateway = mkOption { | ||||
|         type = types.str; | ||||
|         default = "fe80::1"; | ||||
|         description = "IPv6 gateway (documentation only - using SLAAC)"; | ||||
|       }; | ||||
|     }; | ||||
|  | ||||
|     bgp = { | ||||
|       enable = mkOption { | ||||
|         type = types.bool; | ||||
|         default = false; | ||||
|         description = "Enable BGP/FRR for shared IP failover"; | ||||
|       }; | ||||
|  | ||||
|       peers = mkOption { | ||||
|         type = types.listOf types.str; | ||||
|         default = []; | ||||
|         example = [ "2001:db8::1" "2001:db8::2" "2001:db8::3" "2001:db8::4" ]; | ||||
|         description = "List of BGP peer addresses (e.g., Linode route servers)"; | ||||
|       }; | ||||
|  | ||||
|       advertisedSubnets = mkOption { | ||||
|         type = types.listOf types.str; | ||||
|         default = []; | ||||
|         example = [ "2001:db8:1234::/56" ]; | ||||
|         description = "List of IPv6 subnets to advertise via BGP"; | ||||
|       }; | ||||
|  | ||||
|       routeMap = mkOption { | ||||
|         type = types.enum [ "primary" "secondary" ]; | ||||
|         description = "BGP route map type (primary/secondary for HA)"; | ||||
|       }; | ||||
|     }; | ||||
|  | ||||
|     wireguard = { | ||||
|       enable = mkOption { | ||||
|         type = types.bool; | ||||
|         default = false; | ||||
|         description = "Enable WireGuard configuration"; | ||||
|       }; | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   imports = [ ./wireguard.nix ]; | ||||
|  | ||||
|   config = mkIf cfg.enable { | ||||
|     networking.hostName = cfg.hostname; | ||||
|     networking.domain = cfg.domain; | ||||
|     networking.nftables.enable = true; | ||||
|  | ||||
|     # Enable systemd-networkd | ||||
|     systemd.network.enable = true; | ||||
| @@ -36,28 +108,81 @@ in { | ||||
|     # Tools for monitoring/key generation | ||||
|     environment.systemPackages = with pkgs; [ wireguard-tools tcpdump ]; | ||||
|  | ||||
|   # Configure Ethernet interface (eth0) | ||||
|     # Configure network interface - pure SLAAC/DHCP | ||||
|     systemd.network.networks."10-egress" = { | ||||
|     matchConfig.Name = egress.interface; | ||||
|       matchConfig.Name = cfg.interface; | ||||
|       networkConfig = { | ||||
|       DHCP = "no"; | ||||
|       IPv6AcceptRA = "no"; | ||||
|       IPv6PrivacyExtensions = "yes"; | ||||
|         DHCP = "yes"; # Enable both IPv4 and IPv6 DHCP | ||||
|         IPv6AcceptRA = "yes"; | ||||
|         IPv6PrivacyExtensions = "no"; | ||||
|         IPv6Forwarding = "yes"; | ||||
|       }; | ||||
|  | ||||
|     address = [ egress.ipv4.address egress.ipv6.address ]; | ||||
|     gateway = [ egress.ipv4.gateway egress.ipv6.gateway ]; | ||||
|  | ||||
|     dns = resolvers; | ||||
|       dns = cfg.dns; | ||||
|     }; | ||||
|  | ||||
|   boot.kernel.sysctl = { "net.ipv6.conf.all.forwarding" = 1; }; | ||||
|     # Add blackhole routes for advertised subnets on loopback | ||||
|     systemd.network.networks."10-loopback" = mkIf cfg.bgp.enable { | ||||
|       matchConfig.Name = "lo"; | ||||
|       routes = map (subnet: { | ||||
|         Destination = subnet; | ||||
|         Type = "blackhole"; | ||||
|       }) cfg.bgp.advertisedSubnets; | ||||
|     }; | ||||
|  | ||||
|   imports = [ ./wireguard.nix ]; | ||||
|     # Create dummy interface for shared WireGuard endpoint IP (HA setup) | ||||
|     systemd.network.netdevs."20-shared-dummy" = mkIf cfg.bgp.enable { | ||||
|       netdevConfig = { | ||||
|         Kind = "dummy"; | ||||
|         Name = "shared0"; | ||||
|       }; | ||||
|     }; | ||||
|  | ||||
|   wireguard.interfaces = tunnels; | ||||
|     systemd.network.networks."20-shared-dummy" = mkIf cfg.bgp.enable { | ||||
|       matchConfig.Name = "shared0"; | ||||
|       address = [ "2600:3c08:e002:6cff::1/128" ]; | ||||
|     }; | ||||
|  | ||||
|   networking.firewall = { allowedUDPPorts = map (x: x.serverPort) tunnels; }; | ||||
|     boot.kernel.sysctl = { | ||||
|       "net.ipv6.conf.all.forwarding" = 1; | ||||
|       "net.ipv6.conf.${cfg.interface}.accept_ra" = 2; | ||||
|       # Accept prefix info | ||||
|       "net.ipv6.conf.${cfg.interface}.accept_ra_pinfo" = 1; | ||||
|       # Accept default router | ||||
|       "net.ipv6.conf.${cfg.interface}.accept_ra_defrtr" = 1; | ||||
|     }; | ||||
|  | ||||
|     # BGP configuration | ||||
|     # Reference: https://techdocs.akamai.com/cloud-computing/docs/configuring-ip-failover-over-bgp-using-frr-advanced | ||||
|     services.frr.bgpd.enable = cfg.bgp.enable; | ||||
|     services.frr.config = mkIf cfg.bgp.enable (let | ||||
|       peerConfig = concatMapStringsSep "\n" (peer: "  neighbor ${peer} peer-group RS") cfg.bgp.peers; | ||||
|       networkConfig = concatMapStringsSep "\n" (subnet: "  network ${subnet} route-map ${cfg.bgp.routeMap}") cfg.bgp.advertisedSubnets; | ||||
|     in '' | ||||
|       hostname ${cfg.hostname} | ||||
|  | ||||
|       router bgp 65001 | ||||
|       no bgp ebgp-requires-policy | ||||
|       coalesce-time 1000 | ||||
|       bgp bestpath as-path multipath-relax | ||||
|       neighbor RS peer-group | ||||
|       neighbor RS remote-as external | ||||
|       neighbor RS ebgp-multihop 10 | ||||
|       neighbor RS capability extended-nexthop | ||||
|       ${peerConfig} | ||||
|  | ||||
|       address-family ipv6 unicast | ||||
|       ${networkConfig} | ||||
|       redistribute static | ||||
|       neighbor RS activate | ||||
|       exit-address-family | ||||
|  | ||||
|       route-map primary permit 10 | ||||
|       set community 65000:1 | ||||
|       route-map secondary permit 10 | ||||
|       set community 65000:2 | ||||
|  | ||||
|       ipv6 nht resolve-via-default | ||||
|     ''); | ||||
|   }; | ||||
| } | ||||
| @@ -12,7 +12,7 @@ let | ||||
|       serverAddress = mkOption { | ||||
|         type = types.str; | ||||
|         description = | ||||
|           "IPv6 address with CIDR prefix to bind WireGuard (e.g., 2001:db8::1/64)."; | ||||
|           "IPv6 address/prefix to bind WireGuard (e.g., 2001:db8::1/64)."; | ||||
|       }; | ||||
|       serverPort = mkOption { | ||||
|         type = types.port; | ||||
| @@ -20,19 +20,19 @@ let | ||||
|       }; | ||||
|       clientAddress = mkOption { | ||||
|         type = types.str; | ||||
|         description = "Peer IPv6 address with CIDR (e.g., 2001:db8::2/128)."; | ||||
|         description = "Peer IPv6 address/prefix (e.g., 2001:db8::2/128)."; | ||||
|       }; | ||||
|       clientSubnet = mkOption { | ||||
|         type = types.str; | ||||
|         description = "Peer subnet IPv6 CIDR (e.g., 2001:db8::/64)."; | ||||
|         description = "Peer IPv6 subnet (e.g., 2001:db8::/64)."; | ||||
|       }; | ||||
|       serverPrivateKeyFile = mkOption { | ||||
|         type = types.str; | ||||
|         description = "Name of the sops secret containing server private key."; | ||||
|         type = types.path; | ||||
|         description = "Path to server private key file."; | ||||
|       }; | ||||
|       clientPublicKeyFile = mkOption { | ||||
|         type = types.str; | ||||
|         description = "Name of the sops secret containing client public key."; | ||||
|         type = types.path; | ||||
|         description = "Path to client public key file."; | ||||
|       }; | ||||
|     }; | ||||
|   }); | ||||
| @@ -47,14 +47,14 @@ let | ||||
|       "Duplicate values found for '${name}': ${builtins.toString values}"; | ||||
|   }; | ||||
|  | ||||
|   secretAssertions = lib.flatten (map (cfg: [ | ||||
|   fileAssertions = lib.flatten (map (cfg: [ | ||||
|     { | ||||
|       assertion = builtins.hasAttr cfg.serverPrivateKeyFile config.sops.secrets; | ||||
|       message = "Missing sops secret: ${cfg.serverPrivateKeyFile}"; | ||||
|       assertion = cfg.serverPrivateKeyFile != null; | ||||
|       message = "serverPrivateKeyFile must be provided for interface ${cfg.interface}"; | ||||
|     } | ||||
|     { | ||||
|       assertion = builtins.hasAttr cfg.clientPublicKeyFile config.sops.secrets; | ||||
|       message = "Missing sops secret: ${cfg.clientPublicKeyFile}"; | ||||
|       assertion = cfg.clientPublicKeyFile != null; | ||||
|       message = "clientPublicKeyFile must be provided for interface ${cfg.interface}"; | ||||
|     } | ||||
|   ]) interfaces); | ||||
|  | ||||
| @@ -69,34 +69,28 @@ let | ||||
| in { | ||||
|   options.wireguard.interfaces = mkOption { | ||||
|     type = wgConfigType; | ||||
|     default = []; | ||||
|     description = "List of WireGuard interface configurations."; | ||||
|   }; | ||||
|  | ||||
|   config = { | ||||
|     sops.secrets = let | ||||
|       def = { | ||||
|         owner = "systemd-network"; | ||||
|         group = "systemd-network"; | ||||
|       }; | ||||
|     in lib.mkMerge (map (cfg: { | ||||
|       "${cfg.serverPrivateKeyFile}" = def; | ||||
|       "${cfg.clientPublicKeyFile}" = def; | ||||
|     }) interfaces); | ||||
|     assertions = lib.mkAfter (fileAssertions ++ uniquenessAssertions); | ||||
|  | ||||
|     assertions = lib.mkAfter (secretAssertions ++ uniquenessAssertions); | ||||
|     systemd.network.enable = true; | ||||
|  | ||||
|     systemd.network.netdevs = lib.mkMerge (map (cfg: { | ||||
|       "${cfg.interface}" = { | ||||
|         netdevConfig = { | ||||
|           Kind = "wireguard"; | ||||
|           MTUBytes = "1412"; | ||||
|           Name = cfg.interface; | ||||
|         }; | ||||
|         wireguardConfig = { | ||||
|           PrivateKeyFile = config.sops.secrets.${cfg.serverPrivateKeyFile}.path; | ||||
|           PrivateKeyFile = cfg.serverPrivateKeyFile; | ||||
|           ListenPort = cfg.serverPort; | ||||
|         }; | ||||
|         wireguardPeers = [{ | ||||
|           PublicKeyFile = config.sops.secrets.${cfg.clientPublicKeyFile}.path; | ||||
|           PublicKeyFile = cfg.clientPublicKeyFile; | ||||
|           AllowedIPs = [ cfg.clientAddress cfg.clientSubnet ]; | ||||
|         }]; | ||||
|       }; | ||||
| @@ -105,13 +99,11 @@ in { | ||||
|     systemd.network.networks = lib.mkMerge (map (cfg: { | ||||
|       "${cfg.interface}" = { | ||||
|         matchConfig = { Name = cfg.interface; }; | ||||
|         networkConfig = { IPv6Forwarding = "yes"; }; | ||||
|  | ||||
|         address = [ cfg.serverAddress ]; | ||||
|  | ||||
|         routes = [ | ||||
|           { | ||||
|             Destination = cfg.clientAddress; | ||||
|             Scope = "link"; | ||||
|           } | ||||
|           { | ||||
|             Destination = cfg.clientSubnet; | ||||
|             Scope = "link"; | ||||
| @@ -119,5 +111,8 @@ in { | ||||
|         ]; | ||||
|       }; | ||||
|     }) interfaces); | ||||
|  | ||||
|     # Automatically open firewall ports for WireGuard | ||||
|     networking.firewall.allowedUDPPorts = map (cfg: cfg.serverPort) interfaces; | ||||
|   }; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user