you know it's good when you keep rewriting it
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,7 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"root": {}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
@ -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";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
Loading…
Reference in New Issue