Compare commits
	
		
			2 Commits
		
	
	
		
			a2d5333f01
			...
			2f5f746492
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2f5f746492 | |||
| 01a77e65a6 | 
							
								
								
									
										145
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,34 +1,125 @@ | |||||||
| # sixnix | # 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. | NixOS module for IPv6 networking with WireGuard tunnels and 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 | ||||||
|  |  | ||||||
| | Client subnet      | WireGuard subnet    |    | | 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. | ||||||
| |--------------------|---------------------|----| |  | ||||||
| | 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 | |  | ||||||
|  |  | ||||||
| | Host                  | Subnet                      | | Linode provides a routed `/56` for free with their cheapest $5/month VPS. That's 256 `/64` subnets. You get 1TB traffic at 1Gbit (40Gbit input but that's kind of irrelevant for our use case). Good enough. | ||||||
| |-----------------------|-----------------------------| |  | ||||||
| | wireguard.example.com | 2001:0db8:0:ff::/64         | |  | ||||||
|  |  | ||||||
| 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. | ## Solution 1: Simple WireGuard setup | ||||||
|  |  | ||||||
| - set up sops using the [docs here](https://github.com/Mic92/sops-nix) | Just a plain WireGuard tunnel to route the `/56` to your home network. | ||||||
| - 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` | With a `/56` you can carve out a `/60` for WireGuard endpoints and you're left with 15 `/60` subnets. Share it with friends! | ||||||
| - configure egress, firewall, wireguard tunnels etc in `modules/network.nix` |  | ||||||
|  | | Client subnet      | WireGuard subnet     |    | | ||||||
|  | |--------------------|----------------------|----| | ||||||
|  | | 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 | | ||||||
|  |  | ||||||
|  | ### Basic configuration | ||||||
|  |  | ||||||
|  | ```nix | ||||||
|  | { | ||||||
|  |   inputs.sixnix.url = "git+https://code.planet-express.in/konarak/sixnix.git"; | ||||||
|  |  | ||||||
|  |   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: BGP failover (ultra advanced) | ||||||
|  |  | ||||||
|  | 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 bandwidth 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 = { |   inputs = { | ||||||
|     nixpkgs.url = "nixpkgs/nixos-25.05"; |     nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; | ||||||
|     nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; |  | ||||||
|  |  | ||||||
|     sops-nix.url = "github:Mic92/sops-nix"; |  | ||||||
|     sops-nix.inputs.nixpkgs.follows = "nixpkgs"; |  | ||||||
|  |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   outputs = { self, nixpkgs, nixpkgs-unstable, sops-nix, }@attrs: { |   outputs = { self, nixpkgs }: { | ||||||
|     nixosConfigurations = { |     nixosModules.default = import ./modules; | ||||||
|       "tunnels" = nixpkgs.lib.nixosSystem { |     nixosModules.sixnix = import ./modules; | ||||||
|         system = "x86_64-linux"; |  | ||||||
|         specialArgs = attrs; |  | ||||||
|         modules = [ |  | ||||||
|           (import ./config/maa1-ops/configuration.nix) |  | ||||||
|           sops-nix.nixosModules.sops |  | ||||||
|         ]; |  | ||||||
|       }; |  | ||||||
|     }; |  | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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,63 +1,188 @@ | |||||||
| { config, pkgs, ... }: | { config, pkgs, lib, ... }: | ||||||
| let |  | ||||||
|   hostname = "tunnels"; |  | ||||||
|   domain = "example.com"; |  | ||||||
|  |  | ||||||
|   resolvers = [ "9.9.9.9" "2620:fe::fe" ]; | with lib; | ||||||
|  |  | ||||||
|   egress = { | let cfg = config.sixnix; | ||||||
|     interface = "eth0"; |  | ||||||
|  |  | ||||||
|     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"; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   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 { | in { | ||||||
|  |   options.sixnix = { | ||||||
|  |     enable = mkEnableOption "sixnix networking configuration"; | ||||||
|  |  | ||||||
|   networking.hostName = hostname; |     hostname = mkOption { | ||||||
|   networking.domain = domain; |       type = types.str; | ||||||
|  |       description = "Server hostname"; | ||||||
|   # Enable systemd-networkd |  | ||||||
|   systemd.network.enable = true; |  | ||||||
|   networking.useNetworkd = true; |  | ||||||
|  |  | ||||||
|   # Tools for monitoring/key generation |  | ||||||
|   environment.systemPackages = with pkgs; [ wireguard-tools tcpdump ]; |  | ||||||
|  |  | ||||||
|   # Configure Ethernet interface (eth0) |  | ||||||
|   systemd.network.networks."10-egress" = { |  | ||||||
|     matchConfig.Name = egress.interface; |  | ||||||
|     networkConfig = { |  | ||||||
|       DHCP = "no"; |  | ||||||
|       IPv6AcceptRA = "no"; |  | ||||||
|       IPv6PrivacyExtensions = "yes"; |  | ||||||
|       IPv6Forwarding = "yes"; |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     address = [ egress.ipv4.address egress.ipv6.address ]; |     domain = mkOption { | ||||||
|     gateway = [ egress.ipv4.gateway egress.ipv6.gateway ]; |       type = types.str; | ||||||
|  |       default = "notwork.in"; | ||||||
|  |       description = "Domain name"; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     dns = resolvers; |     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"; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   boot.kernel.sysctl = { "net.ipv6.conf.all.forwarding" = 1; }; |  | ||||||
|  |  | ||||||
|   imports = [ ./wireguard.nix ]; |   imports = [ ./wireguard.nix ]; | ||||||
|  |  | ||||||
|   wireguard.interfaces = tunnels; |   config = mkIf cfg.enable { | ||||||
|  |     networking.hostName = cfg.hostname; | ||||||
|  |     networking.domain = cfg.domain; | ||||||
|  |     networking.nftables.enable = true; | ||||||
|  |  | ||||||
|   networking.firewall = { allowedUDPPorts = map (x: x.serverPort) tunnels; }; |     # Enable systemd-networkd | ||||||
|  |     systemd.network.enable = true; | ||||||
|  |     networking.useNetworkd = true; | ||||||
|  |  | ||||||
|  |     # Tools for monitoring/key generation | ||||||
|  |     environment.systemPackages = with pkgs; [ wireguard-tools tcpdump ]; | ||||||
|  |  | ||||||
|  |     # Configure network interface - pure SLAAC/DHCP | ||||||
|  |     systemd.network.networks."10-egress" = { | ||||||
|  |       matchConfig.Name = cfg.interface; | ||||||
|  |       networkConfig = { | ||||||
|  |         DHCP = "yes"; # Enable both IPv4 and IPv6 DHCP | ||||||
|  |         IPv6AcceptRA = "yes"; | ||||||
|  |         IPv6PrivacyExtensions = "no"; | ||||||
|  |         IPv6Forwarding = "yes"; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       dns = cfg.dns; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     # 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; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     # 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"; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     systemd.network.networks."20-shared-dummy" = mkIf cfg.bgp.enable { | ||||||
|  |       matchConfig.Name = "shared0"; | ||||||
|  |       address = [ "2600:3c08:e002:6cff::1/128" ]; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     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 { |       serverAddress = mkOption { | ||||||
|         type = types.str; |         type = types.str; | ||||||
|         description = |         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 { |       serverPort = mkOption { | ||||||
|         type = types.port; |         type = types.port; | ||||||
| @@ -20,19 +20,19 @@ let | |||||||
|       }; |       }; | ||||||
|       clientAddress = mkOption { |       clientAddress = mkOption { | ||||||
|         type = types.str; |         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 { |       clientSubnet = mkOption { | ||||||
|         type = types.str; |         type = types.str; | ||||||
|         description = "Peer subnet IPv6 CIDR (e.g., 2001:db8::/64)."; |         description = "Peer IPv6 subnet (e.g., 2001:db8::/64)."; | ||||||
|       }; |       }; | ||||||
|       serverPrivateKeyFile = mkOption { |       serverPrivateKeyFile = mkOption { | ||||||
|         type = types.str; |         type = types.path; | ||||||
|         description = "Name of the sops secret containing server private key."; |         description = "Path to server private key file."; | ||||||
|       }; |       }; | ||||||
|       clientPublicKeyFile = mkOption { |       clientPublicKeyFile = mkOption { | ||||||
|         type = types.str; |         type = types.path; | ||||||
|         description = "Name of the sops secret containing client public key."; |         description = "Path to client public key file."; | ||||||
|       }; |       }; | ||||||
|     }; |     }; | ||||||
|   }); |   }); | ||||||
| @@ -47,14 +47,14 @@ let | |||||||
|       "Duplicate values found for '${name}': ${builtins.toString values}"; |       "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; |       assertion = cfg.serverPrivateKeyFile != null; | ||||||
|       message = "Missing sops secret: ${cfg.serverPrivateKeyFile}"; |       message = "serverPrivateKeyFile must be provided for interface ${cfg.interface}"; | ||||||
|     } |     } | ||||||
|     { |     { | ||||||
|       assertion = builtins.hasAttr cfg.clientPublicKeyFile config.sops.secrets; |       assertion = cfg.clientPublicKeyFile != null; | ||||||
|       message = "Missing sops secret: ${cfg.clientPublicKeyFile}"; |       message = "clientPublicKeyFile must be provided for interface ${cfg.interface}"; | ||||||
|     } |     } | ||||||
|   ]) interfaces); |   ]) interfaces); | ||||||
|  |  | ||||||
| @@ -69,34 +69,28 @@ let | |||||||
| in { | in { | ||||||
|   options.wireguard.interfaces = mkOption { |   options.wireguard.interfaces = mkOption { | ||||||
|     type = wgConfigType; |     type = wgConfigType; | ||||||
|  |     default = []; | ||||||
|     description = "List of WireGuard interface configurations."; |     description = "List of WireGuard interface configurations."; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   config = { |   config = { | ||||||
|     sops.secrets = let |     assertions = lib.mkAfter (fileAssertions ++ uniquenessAssertions); | ||||||
|       def = { |  | ||||||
|         owner = "systemd-network"; |  | ||||||
|         group = "systemd-network"; |  | ||||||
|       }; |  | ||||||
|     in lib.mkMerge (map (cfg: { |  | ||||||
|       "${cfg.serverPrivateKeyFile}" = def; |  | ||||||
|       "${cfg.clientPublicKeyFile}" = def; |  | ||||||
|     }) interfaces); |  | ||||||
|  |  | ||||||
|     assertions = lib.mkAfter (secretAssertions ++ uniquenessAssertions); |     systemd.network.enable = true; | ||||||
|  |  | ||||||
|     systemd.network.netdevs = lib.mkMerge (map (cfg: { |     systemd.network.netdevs = lib.mkMerge (map (cfg: { | ||||||
|       "${cfg.interface}" = { |       "${cfg.interface}" = { | ||||||
|         netdevConfig = { |         netdevConfig = { | ||||||
|           Kind = "wireguard"; |           Kind = "wireguard"; | ||||||
|  |           MTUBytes = "1412"; | ||||||
|           Name = cfg.interface; |           Name = cfg.interface; | ||||||
|         }; |         }; | ||||||
|         wireguardConfig = { |         wireguardConfig = { | ||||||
|           PrivateKeyFile = config.sops.secrets.${cfg.serverPrivateKeyFile}.path; |           PrivateKeyFile = cfg.serverPrivateKeyFile; | ||||||
|           ListenPort = cfg.serverPort; |           ListenPort = cfg.serverPort; | ||||||
|         }; |         }; | ||||||
|         wireguardPeers = [{ |         wireguardPeers = [{ | ||||||
|           PublicKeyFile = config.sops.secrets.${cfg.clientPublicKeyFile}.path; |           PublicKeyFile = cfg.clientPublicKeyFile; | ||||||
|           AllowedIPs = [ cfg.clientAddress cfg.clientSubnet ]; |           AllowedIPs = [ cfg.clientAddress cfg.clientSubnet ]; | ||||||
|         }]; |         }]; | ||||||
|       }; |       }; | ||||||
| @@ -105,13 +99,11 @@ in { | |||||||
|     systemd.network.networks = lib.mkMerge (map (cfg: { |     systemd.network.networks = lib.mkMerge (map (cfg: { | ||||||
|       "${cfg.interface}" = { |       "${cfg.interface}" = { | ||||||
|         matchConfig = { Name = cfg.interface; }; |         matchConfig = { Name = cfg.interface; }; | ||||||
|  |         networkConfig = { IPv6Forwarding = "yes"; }; | ||||||
|  |  | ||||||
|         address = [ cfg.serverAddress ]; |         address = [ cfg.serverAddress ]; | ||||||
|  |  | ||||||
|         routes = [ |         routes = [ | ||||||
|           { |  | ||||||
|             Destination = cfg.clientAddress; |  | ||||||
|             Scope = "link"; |  | ||||||
|           } |  | ||||||
|           { |           { | ||||||
|             Destination = cfg.clientSubnet; |             Destination = cfg.clientSubnet; | ||||||
|             Scope = "link"; |             Scope = "link"; | ||||||
| @@ -119,5 +111,8 @@ in { | |||||||
|         ]; |         ]; | ||||||
|       }; |       }; | ||||||
|     }) interfaces); |     }) interfaces); | ||||||
|  |  | ||||||
|  |     # Automatically open firewall ports for WireGuard | ||||||
|  |     networking.firewall.allowedUDPPorts = map (cfg: cfg.serverPort) interfaces; | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user