you know it's good when you keep rewriting it

release
Matthew Salerno 1 year ago
parent e58ea8655e
commit 5a3c11f81f

@ -1,145 +0,0 @@
{lib}: wirenix-config:
with lib.attrsets;
with lib.lists;
with lib.trivial;
with builtins;
let
# Math
# extract-key :: string -> list -> attrSet
# Example:
# listOfSetsToSetByKey "primary" [ {primary = "foo"; secondary = 1; tertiary = "one"} {primary = "bar"; secondary = 2; tertiary = "two"} ]
# {foo = {secondary = 1; tertiary = "one"}; bar = {secondary = 2; tertiary = "two"};}
listOfSetsToSetByKey = key: list:
listToAttrs (
forEach list (elem: {
name = elem."${key}";
value = removeAttrs elem [ key ];
})
);
# returns true if `elem` is in `list`
inList = elem: list: any (e: e == elem) list;
# adds colons to a string every 4 characters for IPv6 shenanigans
add-colons = string:
if ((stringLength string) > 4)
then
((substring 0 4 string) + ":" + (add-colons (substring 4 32 string)))
else string;
# Peer Information
# Helper functions for querying data about peers from the config
peerInfo = {
# last 20 (hardcoded atm) characters (80 bits) of the peer's IPv6 address
ipSuffix = peerName: substring 0 20 (hashString "sha256" peerName);
# list of subnets (as subnet attrset in wirenix config) the peer belongs to
subnets = peer: filter (subnet: inList subnet.name peer.subnets) wirenix-config.subnets;
# list of groups the peer connects to
groupConnections = peer: catAttrs "group" peer.connections;
# list of peers the peer connects to
directConnections = peer: catAttrs "peer" peer.connections;
# list of subnets for which the peer will try to connect to all peer in the subnet
subnetConnections = peer: catAttrs "subnet" peer.connections;
# gets the peer (as peer attrset in wirenix config) from a name
peerFromName = peerName: head (filter (peer: peer.name == peerName) wirenix-config.peers);
# gets all peers (as peer attrset in wirenix config) the are in a group
peersInGroup = groupName: filter (peer: inList groupName peer.groups) wirenix-config.peers;
# returns true if peer is in group
peerIsInGroup = peer: groupName: inList peer (peerInfo.peersInGroup groupName);
# gets all peers (as peer attrset in wirenix config) that the given peer connects to, may contain the peer itself and duplicates
connectionsUnfiltered = peer:
(map (peerInfo.peerFromName) (peerInfo.directConnections peer)) ++
(concatMap (group: peerInfo.peersInGroup group) (peerInfo.groupConnections peer)) ++
(concatMap (subnet: subnetInfo.peersInSubnet subnet) (peerInfo.subnetConnections peer));
# gets all peers (as peer attrset in wirenix config) that the given peer connects to, will not contain the peer itself or duplicates
connections = peer: remove peer (unique (peerInfo.connectionsUnfiltered peer));
# returns the peer's IP when given the peer's name and subnet name
ip = subnetName: PeerName: (add-colons ((subnetInfo.prefix subnetName) + (peerInfo.ipSuffix PeerName))) + "/80";
# returns the endpoint for peerTo that peerFrom will connect with
endpointMatches = peerFrom: peerTo: map (matched: removeAttrs matched [ "match" ]) (
filter (endpoint:
if endpoint == {} then true else
all (id) (
mapAttrsToList (type: value:
if (type == "group") then
(peerInfo.peerIsInGroup peerFrom value)
else if (type == "peer") then
(peerFrom.name == endpoint.match.peer)
else if (type == "subnet") then
(peerInfo.peerIsInSubnet peerFrom (subnetInfo.subnetFromName value))
else throw "Unexpected type "+type+" in endpoints config."
) endpoint
)
) peerTo.endpoints
);
endpoint = foldl' (mergeAttrs) {} endpointMatches;
};
# Subnet Information
# Helper functions for querying data about subnets from the config
subnetInfo = {
# gets all peers (as peer attrset in wirenix config) that belong to the subnet
peersInSubnet = subnet: filter (peer: inList subnet.name peer.subnets) wirenix-config.peers;
# returns true if peer is in subnet
peerIsInSubnet = subnet: peer: inList peer (subnetInfo.peersInSubnet subnet);
# gets the subnet (as subnet attrset in wirenix config) from a name
subnetFromName = subnetName: head (filter (subnet: subnet.name == subnetName) wirenix-config.subnets);
# gets the first 10 characters of the IPV6 address for the subnet name
prefix = subnetName: "fd" + (substring 0 10 (hashString "sha256" subnetName));
};
# Mappers take a peer or subnet from the config and convert it
# into a recursive attrset that is better suited for nix configs
mappers = rec {
# Maps a wirenix config subnet to a recursive attrset, structure is as follows:
# peers: a set of peers (as similar recursive attrsets), keyed by name
# ip: the subnet ip in CIDR notation
subnetMap = subnet: {
name = subnet.name;
peers = listOfSetsToSetByKey "name" (map (peerMap) (subnetInfo.peersInSubnet subnet));
ip = (add-colons (subnetInfo.prefix subnet.name)) + "::/80";
};
# Maps a wirenix config peer to a recursive attrset, structure is as follows:
# subnets: a set of subnets (as similar recursive attrsets), keyed by name, with the following structure:
# subnet: the subnet as described in the subnetMap function's description
# ip: the peer's ip on the subnet (can only be one IP at the moment)
# peers: peers (as recursive attrset) on the subnet that the current peer has connections to, keyed by name, with the following structure:
# peer: the peer as described by this description
# ip: the connected peers ip on the subnet (same as subnets[subnetName].peers[peerName].subnets[subnetName].ip)
# endpoint: the endpoint to connect to, described as follows:
# ip: the ip to connect to
# port: the port to connect to
# persistentKeepalive: (optional) see networking.wireguard.interfaces.<name>.*.persistentKeepalive
# dynamicEndpointRefreshSeconds: (optional) see networking.wireguard.interfaces.<name>.*.dynamicEndpointRefreshSeconds
# dynamicEndpointRefreshRestartSeconds: (optional) see networking.wireguard.interfaces.<name>.*.dynamicEndpointRefreshRestartSeconds
# connections: a set of all peers (as recursive attrset) which the current peer connects to, keyed by name
# publicKey: the peer's public key
# privateKeyFile: the location of the peer's private key
peerMap = peer: {
name = peer.name;
subnets = listOfSetsToSetByKey "name" (
map (subnet: {
name = subnet.name;
subnet = removeAttrs (subnetMap subnet) [ "name" ];
ip = peerInfo.ip subnet.name peer.name;
peers = listOfSetsToSetByKey "name" (
(map (peerTo: {
name = peerTo.name;
peer = removeAttrs (peerMap peerTo) [ "name" ];
ip = peerInfo.ip subnet.name peerTo.name;
endpoint = peerInfo.endpoint peer peerTo;
}) (filter (otherPeer: inList subnet.name otherPeer.subnets) (peerInfo.connections peer)))
);
}) (peerInfo.subnets peer)
);
connections = listOfSetsToSetByKey "name" (map (peerMap) (peerInfo.connections peer));
publicKey = peer.publicKey;
privateKeyFile = peer.privateKeyFile;
};
};
in
{
peerFromName = removeAttrs (mappers.peerMap peerInfo.peerFromName) [ "name" ];
subnetFromName = removeAttrs (mappers.subnetMap subnetInfo.subnetFromName) [ "name" ];
peers = listOfSetsToSetByKey "name" (map (mappers.peerMap) (wirenix-config.peers));
subnets = listOfSetsToSetByKey "name" (map (mappers.subnetMap) (wirenix-config.subnets));
}

@ -1,5 +1,4 @@
{config, lib, ...}: intermediateConfig:
{config, lib, ...}:
with lib.trivial; with lib.trivial;
with lib.attrsets; with lib.attrsets;
with lib.lists; with lib.lists;
@ -7,25 +6,23 @@ with lib;
let let
# check whether or not agenix-rekey exists # check whether or not agenix-rekey exists
has-rekey = config ? rekey; has-rekey = config ? rekey;
# The remapper transforms the config in a way that makes filling out configs more easy thisPeer = intermediateConfig.peers."${config.wirenix.peerName}";
remapper = import ./config-remapper.nix {inherit lib;} config.modules.wirenix.config;
thisPeer = remapper.peerFromName config.wirenix.peerName;
# these aren't really important, I just wanted to reverse the argument order # these aren't really important, I just wanted to reverse the argument order
forEachAttr = flip mapAttrs'; forEachAttr' = flip mapAttrs';
forEachAttrToList = flip mapAttrsToList; forEachAttrToList = flip mapAttrsToList;
in in
{ {
networking.wireguard = { networking.wireguard = {
interfaces = forEachAttr thisPeer.subnets (name: subnetConnection: { name = "wg-${name}"; interfaces = forEachAttr' thisPeer.subnetConnections (name: subnetConnection: { name = "wg-${name}";
value = { value = {
ips = [ subnetConnection.ip ]; ips = subnetConnection.ipAddresses;
listenPort = subnetConnection.subnet.defaultPort; listenPort = subnetConnection.listenPort;
privateKeyFile = thisPeer.privateKeyFile; privateKeyFile = thisPeer.privateKeyFile;
peers = forEachAttrToList subnetConnection.peers (peerName: peerConnection: mkMerge [ peers = forEachAttrToList subnetConnection.peerConnections (peerName: peerConnection: mkMerge [
{ {
name = peerName; name = peerName;
publicKey = peerConnection.peer.publicKey; publicKey = peerConnection.peer.publicKey;
allowedIPs = [ peerConnection.ip ]; allowedIPs = peerConnection.ipAddresses;
endpoint = "${peerConnection.endpoint.ip}:${peerConnection.endpoint.port}"; endpoint = "${peerConnection.endpoint.ip}:${peerConnection.endpoint.port}";
} }
mkIf (peerConnection.endpoint ? persistentKeepalive) { mkIf (peerConnection.endpoint ? persistentKeepalive) {

@ -1,7 +0,0 @@
{
"nodes": {
"root": {}
},
"root": "root",
"version": 7
}

1357
lib.nix

File diff suppressed because it is too large Load Diff

@ -0,0 +1,124 @@
{lib, ...}: v1_acl:
with lib.attrsets;
with lib.lists;
with lib.trivial;
with (import ../lib.nix);
with builtins;
let
/** parsePeer :: acl_peer -> ic_peer */
parsePeer = acl_peer: {
subnetConnections = listOfSetsToSetByKey "name" (pipeMap [subnetFromName (getSubnetConnectionAndName acl_peer)] acl_peer.subnets);
publicKey = acl_peer.publicKey;
privateKeyFile = acl_peer.privateKeyFile;
} //
(if acl_peer ? extraArgs then {extraArgs = acl_peer.extraArgs;} else {}) //
{
publicKey = acl_peer.publicKey;
privateKeyFile = acl_peer.privateKeyFile;
} //
(if acl_peer ? groups then {groups = map groupFromName acl_peer.groups;} else {groups = [];});
/** parseGroup :: acl_group -> ic_group */
parseGroup = acl_group: {
peers = mapListOfSetsToSetByKey parsePeer (selectPeers [{type="group"; rule="is"; value="${acl_group.name}";}]);
} // (if acl_group ? extraArgs then {extraArgs = acl_group.extraArgs;} else {});
/** parseSubnet :: acl_subnet -> ic_subnet */
parseSubnet = acl_subnet: {
peers = mapListOfSetsToSetByKey parsePeer (selectPeers [{type="subnet"; rule="is"; value="${acl_subnet.name}";}]);
} // (if acl_subnet ? extraArgs then {extraArgs = acl_subnet.extraArgs;} else {});
/** getSubnetConnection :: acl_peer -> acl_subnet -> (subnetConnection // {name}) */
getSubnetConnectionAndName = acl_peer: acl_subnet: {
name = acl_subnet.name; # name gets removed shortly after, name is not in the actual subnetConnection object
subnet = parseSubnet acl_subnet;
ipAddresses = getIpAddresses acl_peer acl_subnet;
listenPort = acl_peer.subnets."${acl_subnet.name}".listenPort;
peerConnections = getPeerConnections acl_peer acl_subnet;
} // (if acl_peer.subnets."${acl_subnet.name}" ? extraArgs then {extraArgs = acl_peer.subnets."${acl_subnet.name}".extraArgs;} else {});
/** getIpAddresses :: acl_peer -> acl_subnet -> [str] */
getIpAddresses = acl_peer: acl_subnet:
if (acl_peer.subnets."${acl_subnet.name}" ? ipAddresses) then (
if (elem "auto" acl_peer.subnets."${acl_subnet.name}".ipAddresses) then (
(remove "auto" acl_peer.subnets."${acl_subnet.name}".ipAddresses) ++ (singleton (generateIPv6Address acl_peer.name acl_subnet.name))
) else acl_peer.subnets."${acl_subnet.name}".ipAddresses
) else (singleton (generateIPv6Address acl_peer.name acl_subnet.name));
/** getPeerConnections :: acl_peer -> acl_subnet -> str -> peerConnection */
getPeerConnections = acl_peerFrom: acl_subnet:
let
filterSubnets = connection: elem acl_subnet.name connection.subnets;
filterPeer = key: acl_peer: connection: elem acl_peer.name (catAttrs "name" (selectPeers connection."${key}"));
getConnectionsX = key: filter (connection: all (x: x connection) [filterSubnets (filterPeer key acl_peerFrom)]) v1_acl.connections;
getConnectionsA = getConnectionsX "a";
getConnectionsB = getConnectionsX "b";
allPeers = unique ((concatMap (connection: selectPeers connection.b) getConnectionsA) ++ (concatMap (connection: selectPeers connection.a) getConnectionsB));
allOtherPeers = remove acl_peerFrom allPeers;
getExtraArgs = acl_peerTo:
let
connections = (filter (filterPeer "a" acl_peerTo) getConnectionsB) ++ (filter (filterPeer "b" acl_peerTo) getConnectionsA);
extraArgsList = catAttrs "extraArgs" connections;
in
foldl' mergeAttrs {} extraArgsList;
in
listOfSetsToSetByKey "name" (map (acl_peerTo:
{
name = acl_peerTo.name;
peer = parsePeer acl_peerTo;
ipAddresses = getIpAddresses acl_peerTo acl_subnet;
endpoint = getEndpoint acl_peerFrom acl_peerTo;
extraArgs = getExtraArgs acl_peerTo;
}) allOtherPeers);
/** getEndpoint :: acl_peer -> acl_peer -> ic_endpoint */
getEndpoint = acl_peerFrom: acl_peerTo:
let
getAllEndpointMatches = filter (endpoint: elem acl_peerFrom.name (catAttrs "name" (selectPeers (if endpoint ? match then endpoint.match else [])))) acl_peerTo.endpoints;
in
removeAttrs (foldl' mergeAttrs {} getAllEndpointMatches) [ "match" ];
/** selectPeers :: [acl_filters] -> str -> [acl_peer]
* (str -> ic_peer) means it returns an attrset of peers keyed by name, typescript syntax:
* selectPeers(acl: acl, acl_filters: acl_filter[]): {[peerName: string]: ic_peer};
*/
selectPeers = acl_filters:
if length acl_filters == 0
then
v1_acl.peers
else
foldl' intersectAttrs (selectPeersSingleFilter (head acl_filters)) (map selectPeersSingleFilter acl_filters);
/** selectPeersSingleFilter :: acl_filter -> [acl_peer] */
selectPeersSingleFilter = acl_filter:
with acl_filter;
let
applyRule = comparison: if rule == "is" then comparison else if rule == "not" then !comparison else throw ("Unknown filter rule " + rule);
in
if type == "peer" then
(filter (acl_peer: applyRule (acl_peer.name == value)) v1_acl.peers)
else if type == "group" then
(filter (acl_peer: applyRule (elem value acl_peer.groups)) v1_acl.peers)
else if type == "subnet" then
(filter (acl_peer: applyRule (elem value (attrNames acl_peer.subnets))) v1_acl.peers)
else throw ("Unknown filter type " + type);
groupFromName = groupName: findSingle
(group: group.name == groupName)
(throw "No group " + groupName)
(throw "Multiply defined group " + groupName)
v1_acl.groups;
subnetFromName = subnetName: findSingle
(subnet: subnet.name == subnetName)
(throw "No subnet " + subnetName)
(throw "Multiply defined subnet " + subnetName)
v1_acl.subnets;
in
{
peers = mapListOfSetsToSetByKey parsePeer v1_acl.peers;
subnets = mapListOfSetsToSetByKey parseSubnet v1_acl.subnets;
groups = mapListOfSetsToSetByKey parseGroup v1_acl.groups;
}

@ -0,0 +1,307 @@
WireNix is a Nix Flake designed to make creation of Wireguard mesh networks
easier. The simplist and most likely layout is a full mesh network, but Wirenix
is able to support arbitrary graph topologies.
# Reading the README
Due to Nix's typeless nature, I have opted to define all my configurations in
psuedo-typescript to make options more legible. I have chosen typescript
because it looks somewhat like JSON and is easy to understand. Examples will
still be given in Nix EL.
You can start by reading the `ACL Configuration` section, then reading
`Quick Start` section for how to use configure your machines. Other sections
exist to provide helpful context and advanced usage, but should not be
necessary for a working setup.
# Glosary
## ACL
Access Control List:
This is your shared configuration for the network.
## Subnet
In Wirenix, the word subnet represents any network of connected peers.
In the implementation, subnets are keyed by their `name` property. Subnet names
define the initial 32 bits after `fd` in of an the IPv6 addresses peers
connecting to the subnet will use. Generally speaking, one subnet = one
wireguard interface for each client on the subnet.
## Peer
In Wirenix, peer is any machine with a unique public key In the
implementation, peer names define last 80 bits of their IPv6 address.
## Group
In Wirenix, a group is just a tag that peers can have. These are used for
matching peers and can contain arbitrary names.
## Endpoint
In wirenix, an endpoint specifies external IP of a peer that other peers should
connect to.
In the ACL configuration, endpoints can exist on subnets, groups, and peers,
but these are just for convenience. Think of adding an endpoint to a subnet or
group as being the same as adding the endpoint to all peers in the subnet or
group.
Endpoints have filters, which can specify for which connecting clients the
endpoint will apply to.
## Filter
In Wirenix, a filter is used to select peers by their subnets, groups, and
names. A filter is made up of filter rules, specifying multiple rules will
yield the intersection of those rules.
Note that selecting by peer name will always return a list of 1 or 0 entries,
on account of names needing to be unique.
# ACL Configuration
The ACL is a nix attrset designed to be represented in JSON for easy importing
and potential use outside of the nix ecosystem.
## top level acl:
```typescript
type ACL = {
version?: str;
subnets: subnet[];
groups: group[];
peers: peer[];
connections: connection[];
extraArgs?: any;
};
```
Version is used to check for config compatibility and is recommended. Not
specifying version will parse the configuration with the most recent parser
available and generate a warning. Using an older configuration version than
available will use the parser for that version and generate a warning. Using
a version newer than any parsers available will throw an error.
## subnet:
```typescript
type subnet = {
name: str;
endpoints?: endpoint[];
extraArgs?: any;
};
```
## Group:
```typescript
type group = {
name: str;
endpoints?: endpoint[];
extraArgs?: any;
};
```
## Peer:
```typescript
type peer = {
name: str;
subnets: [subnetName: str]: {
listenPort: int;
ipAddresses?: str[];
extraArgs?: any;
};
groups?: str[];
endpoints?: endpoint[];
extraArgs?: any;
};
```
## Connection:
```typescript
type connection = {
a: filter;
b: filter;
subnets: str[];
extraArgs?: any;
};
```
Connections connect all peers matching filter `a` to all peers matching
filter `b`, and all peers matching filter `b` to all peers matching filter `a`
subnets filters the connection to only be made over the subnets listed. It is
recomended to use the subnets property iff the `subnet` filter is also used
(the `subnet` filter on its own will connect all shared subnets of machines in
`a` and `b`, even subnets not mentioned in the filters if they are shared).
## Endpoint:
```typescript
type endpoint = {
match?: filter;
ip?: str;
port?: int;
persistentKeepalive?: int;
dynamicEndpointRefreshSeconds?: int;
dynamicEndpointRefreshRestartSeconds?: int;
extraArgs?: any;
};
```
Endpoints are merged in this order: First lists of endpoints are merged top to
bottom, with the bottom endpoints overriding the top ones. Then, lists are
merged in this order: subnet -> group -> peer; with peer being the highest
priority, overriding others. A good layout is to set ports in subnet, ip in
peer, and leave group empty. group endpoints can be useful for specifying
connection details across port forwarded NATs, however.
## Filter:
```typescript
type filter = {
type: ["peer" | "group" | "subnet"];
rule: [ "is" | "not" ];
value: str;
}[]; // <==== Important! It's a list
```
## extraArgs
`extraArgs` is intentionally left alone. I promise I won't ever set
`extraArgs`, but any value in it will be forwarded on to the corresponding
section in the intermediate configuration. Because of this, it can be used to
pass data into user defined Configuration Modules. Most users can ignore
`extraArgs`.
# Architecture
WireNix consists of 4 main components:
1. The shared ACL Configuration
2. Parser Modules
3. The intermediate Configuration
4. Configuration Modules
The goal of splitting WireNix into modules is both for my own sanity when
developing, and to make it hackable without requiring users to make their own
fork. Users are able to specify their own Parser Modules, enabling them to use
their own preferred ACL syntax if they desire. Users can also specify their own
configuration modules, allowing them to add compatibility to for other network
stacks or to enable their own modules. Using both custom Parser and
Configuration modules enables essentially rewriting this flake however you see
fit, all without making a fork (although at that point I may question why you
don't write your own module from scratch).
## ACL
The shared ACL configuration should describe the full network topology. It does
not need to consist only of NixOS peers (although at the moment, other peers
will have to be configured manually to conform to the expected settings). The
details of this file are documented in the `Top Level ACL` section.
You can make your own ACL configuration format so long as you keep the
`version` field and set it to some unique name. You can then register your
parser which takes your ACL and produces an intermediate configuration like so:
TODO
## Parser Modules
Parser Modules are responsible for taking an ACL and converting it to the
intermediate configuration format. Parser modules are selected by matching the
ACL version field. A parser module must take an ACL and return the
corresponding Intermediate Configuration You can register your own parser
module like so:
TODO
## Intermediate Configuration
The Intermediate Configuration is a recursive attrset that is more suited for
being used in a NixOS configuration than the ACL Configuration.
Unlike the ACL, the intermediate configuration is more verbose, easier to
traverse, repeats itself often, and is recursive. This allows cross version
compatibility so long as the intermediate configuration doesn't change. Any
changes will likely only be the addition of optional features that do not
interfere with existing intermediate configuration use, though at this stage
there are no guarentees.
It can be assumed that all types mentioned are types for the intermediate
connection and NOT the related to types in the ACL. The intermediate
configuration has the following structure:
### Root Structure
```typescript
type intermediateConfiguration = {
peers: {[peerName: string]: peer};
subnets: {[subnetName: string]: subnet};
groups: {[groupName: string]: group};
}
```
### Peer
```typescript
type peer = {
subnetConnections: {[subnetName: string]: subnetConnection};
groups: {[groupName: string]: group}
publicKey: string;
privateKeyFile: string;
extraArgs?: any;
};
```
### Subnet
```typescript
type subnet = {
peers: {[peerName: string]: peer};
extraArgs?: any;
};
```
### Group
```typescript
type group = {
peers: {[peerName: string]: peer};
extraArgs?: any;
};
```
### Peer Connection
```typescript
type peerConnection = {
peer: peer;
ipAddresses: string[];
endpoint: endpoint;
extraArgs?: any;
};
```
### Subnet Connection
```typescript
type subnetConnection = {
subnet: subnet;
ipAddresses: string[];
listenPort: int;
peerConnections: {[peerName: string]: peerConnection};
extraArgs?: any;
};
```
### Endpoint
```typescript
type endpoint = {
ip: str;
port: int;
persistentKeepalive?: int;
dynamicEndpointRefreshSeconds?: int;
dynamicEndpointRefreshRestartSeconds?: int;
extraArgs?: any;
};
```
Unlike the ACL, this structure is recursive, resembling an arbitrary graph.
This graph can be traversed back and forth in circles until you run out of
stack space.
## Configuration Modules
Configuration Modules take the Intermediate Configuration and produce NixOS
configurations from them. By default, there exist configuration modules for
setting up wireguard with the static network configuration, networkd, and
Network Manager. There is a fourth, "default" configuration module that
intelligently selects which module to use (with priority being networkd >
network manager > static configuration). However, you can manually override
which module is used (or use your own module) in your flake.nix file:
TODO
# Integrations:
By default, WireNix supports setting wireguard keypairs with agenix-rekey if
the module is detected (via the existence of `config.rekey`). WireNix also
supports networkd, network manager, and the nixos static network configuration
(in that order of preference).
# Current Issues / Drawbacks
- To keep configuration and the initial POC simpler, assigning IP addresses
manually on subnets is not supported (yet).
- WireNix does not do NAT traversal, it's up to you to forward the correct
ports on your NAT device(s) and apply the correct firewall rules on your
router(s).
- WireNix does not allow for dynamic addition of peers. If you need something
more dynamic, look into Tailscale/Headscale.
- Peers cannot have multiple keys. If this is a desirable feature I may think
of adding it, but I cannot think of a good reason for it.

@ -1,120 +0,0 @@
{
version = "1";
subnets = [
{ name = "subnet.one"; defaultPort = 51820; }
{ name = "subnet.two"; defaultPort = 51821; }
{ name = "subnet.three"; defaultPort = 51822; }
];
peers = [
{
name = "peer.zero";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [
];
groups = [
];
connections = [
{ group = "everyoneConnectsToMe"; }
];
privateKeyFile = "/not/yet";
publicKey = "testData";
presharedKeyFile = "testData2";
}
{
name = "peer.one";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820; persistentKeepalive = 15;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [
"subnet.one"
];
groups = [
];
connections = [
{ group = "everyoneConnectsToMe"; }
{ group = "subnet one group"; }
];
privateKeyFile = "/not/yet";
publicKey = "testData";
presharedKeyFile = "testData2";
}
{
name = "peer.two";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [
"subnet.one"
"subnet.two"
];
groups = [
"everyoneConnectsToMe"
"subnet two group"
];
connections = [
{ group = "everyoneConnectsToMe"; }
{ group = "subnet two group"; }
];
privateKeyFile = "/not/yet";
publicKey = "testData";
presharedKeyFile = "testData2";
}
{
name = "peer.three";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [
"subnet.one"
"subnet.two"
"subnet.three"
];
groups = [
"everyoneConnectsToMe"
"subnet two group"
];
connections = [
{ group = "everyoneConnectsToMe"; }
{ group = "subnet two group"; }
];
privateKeyFile = "/not/yet";
publicKey = "testData";
presharedKeyFile = "testData2";
}
{
name = "peer.four";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [
"subnet.three"
"subnet.one"
];
groups = [
];
connections = [
{ group = "everyoneConnectsToMe"; }
{ peer = "peer.one"; }
];
privateKeyFile = "/not/yet";
publicKey = "testData";
presharedKeyFile = "testData2";
}
];
}

@ -1,127 +1,5 @@
{ config, lib, pkgs, ... }: { config, lib, ... }@inputs:
with lib; with lib;
let
has-rekey = config ? rekey;
peerOpts = {
options = {
subnets = mkOption {
default = [];
type = with types; listOf str;
description = ''
subnets the peer belongs to
'';
};
groups = mkOption {
default = true;
type = with types; listOf str;
description = ''
groups the peer belongs to
'';
};
peers = {
default = true;
type = with types; listOf set;
description = mdDoc ''
Peers the peer is connected to, can be one of `{ peer = "peerName"; }`
or `{ group = "groupname"; }`. Remember to configure this for *both* peers.
The best way to do this is with a simple full mesh network, where all peers
belong to one group ("groupA"), and their peers are `{ group = "groupA"}`.
'';
};
privateKeyFile = mkOption {
example = "/private/wireguard_key";
type = with types; nullOr str;
default = null;
description = mdDoc ''
Private key file as generated by {command}`wg genkey`.
'';
};
name = mkOption {
example = "bernd";
type = types.str;
description = mdDoc "Unique name for the peer (must be unique for all subdomains this peer is a member of)";
};
endpoints = mkOption {
example = ''
[
{match = {}; ip = "192.168.1.10"; port = 51820;} # default case
{match = {group = "location1"; subnet = "lanNet";}; ip = "192.168.1.10"; port = 51820; }
{match = {peer = "offSitePeer1";}; ip = "123.123.123.123"; port = 51825; persistentKeepalive = 15;}
];
'';
type = with types; listOf attrset;
description = mdDoc ''
The endpoints clients use to reach this host with rules to match by
group name `match = {group = "groupName";};`
peer name `match = {peer = "peerName";};`
or a default match at the end `match = {};`
All rules in `match` must be true for a match to happen.
Multiple matches will be merged top to bottom, so rules at the top
should be your most general rules which get overridden.
Values other than `match` specify options for the connection,
possible values are:
- ip
- port
- persistentKeepalive
- dynamicEndpointRefreshSeconds
- dynamicEndpointRefreshRestartSeconds
'';
};
publicKey = mkOption {
example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
type = types.singleLineStr;
description = mdDoc "The base64 public key of the peer.";
};
presharedKeyFile = mkOption {
default = null;
example = "/private/wireguard_psk";
type = with types; nullOr str;
description = mdDoc ''
File pointing to preshared key as generated by {command}`wg genpsk`.
Optional, and may be omitted. This option adds an additional layer of
symmetric-key cryptography to be mixed into the already existing
public-key cryptography, for post-quantum resistance.
'';
};
};
};
subnetOpts = {
options = {
name = mkOption {
default = "wireguard";
example = "mySubnet.myDomain.me";
type = types.str;
description = mdDoc "Unique name for the subnet";
};
defaultPort = mkOption {
example = 51820;
type = types.int;
description = mdDoc ''
The port peers will use when communicating over this subnet.
Currently there is no way to set the ports for peers individually.
'';
};
};
};
configOpts = {
options = {
subnets = mkOption {
default = {};
type = with types; listOf (submodule subnetOpts);
description = ''
Subnets in the mesh network(s)
'';
};
peers = mkOption {
default = {};
type = with types; listOf (submodule peerOpts);
description = ''
Peers in the mesh network(s)
'';
};
};
};
in
{ {
options = { options = {
wirenix = { wirenix = {
@ -142,9 +20,33 @@ in
`wirenix.config.peers.*.name` `wirenix.config.peers.*.name`
''; '';
}; };
config = mkOption { configurer = mkOption {
default = "auto";
type = types.str;
description = mdDoc ''
Configurer to use. Builtin values can be "auto", "networkmanager", or "networkd".
See the `additionalConfigurers` for adding more options.
'';
};
additionalConfigurers = mkOption {
default = "auto";
type = with types; attrsOf (functionTo attrset);
description = mdDoc ''
Additional configurers to load, with their names being used to select from the
configurer option.
'';
};
additionalParsers = mkOption {
default = "auto";
type = with types; attrsOf (functionTo attrset);
description = mdDoc ''
Additional parsers to load, with their names being used to compare to the acl's
"version" feild.
'';
};
aclConfig = mkOption {
default = {}; default = {};
type = with types; setOf (submodule configOpts); type = types.attrset;
description = '' description = ''
Shared configuration file that describes all clients Shared configuration file that describes all clients
''; '';
@ -154,12 +56,21 @@ in
# --------------------------------------------------------------- # # --------------------------------------------------------------- #
config = lib.mkIf (config.modules.wirenix.enable) (lib.mkMerge [ config =
(lib.mkIf (has-rekey) { let
environment.etc.rekey.text = "yes"; configurers = rec {
}) auto = static;
(lib.mkIf (!has-rekey ) { static = import ./configurers/static.nix;
environment.etc.rekey.text = "no"; networkd = import ./configurers/networkd.nix;
}) networkmanager = import ./configurers/networkmanager.nix;
]); } // config.modules.wirenix.additionalConfigurers;
parsers = {
v1 = import ./parsers/v1.nix;
} // config.modules.wirenix.additionalParsers;
acl = config.modules.wirenix.aclConfig;
parser = parsers."${acl.version}" inputs;
configurer = configurers."${config.modules.wirenix.configurer}" inputs;
in
lib.mkIf (config.modules.wirenix.enable)
configurer (parser acl);
} }
Loading…
Cancel
Save