Howdy,
tl;dr :
I wrote some nix code that works and I cargo-culted a nix module
from an existing one that seems to work as well, but I would like
some help in getting the functionality from my nix code into the module.
Lately Iβve been using a bunch of tinc VPN networks and while the supplied
tinc
module on NixOS works pretty great, per network I can create a
network.nix
and import it into my configuration:
{ config, lib, pkgs, ... }:
{
networking.firewall.allowedTCPPorts = [
655
];
networking.firewall.allowedUDPPorts = [
655
];
networking.interfaces."tinc.example".ipv4.addresses = [
{
address = "10.0.0.1";
prefixLength = 24;
}
];
services.tinc.networks = {
example = {
name = "node0";
hosts = {
node0 = ''
Address = 192.168.122.1
Subnet = 10.0.0.1
Subnet = 192.168.0.0/24
Ed25519PublicKey = ...
-----BEGIN RSA PUBLIC KEY-----
...
-----END RSA PUBLIC KEY-----
'';
node1 = ''
Subnet = 10.0.0.2
Port = 655
Ed25519PublicKey = ...
-----BEGIN RSA PUBLIC KEY-----
...
-----END RSA PUBLIC KEY-----
'';
node2 = ''
Address = 192.168.122.3
Subnet = 10.0.0.3
Ed25519PublicKey = ...
-----BEGIN RSA PUBLIC KEY-----
...
-----END RSA PUBLIC KEY-----
'';
};
};
};
}
However after some time, I noticed two things:
- my config files grew into an unmaintainable mess as the number of hosts
inside the VPNs increased - Since
tinc
is a mesh VPN it is a nice Idea to deploy the keys of all
participating nodes to all participating nodes and doing this inside the configuration
At this point I did a bit of searching and found out that it should be possible to use builtins.readFile
:
{ config, lib, pkgs, ... }:
{
networking.firewall.allowedTCPPorts = [
655
];
networking.firewall.allowedUDPPorts = [
655
];
networking.interfaces."tinc.example".ipv4.addresses = [
{
address = "10.0.0.1";
prefixLength = 24;
}
];
services.tinc.networks = {
example = {
name = "node0";
hosts = {
node0 = (builtin.readFile /path/to/node0-config);
node1 = (builtin.readFile /path/to/node1-config);
node2 = (builtin.readFile /path/to/node2-config);
};
};
};
}
The above solves the problem of my configuration files becoming long, but I
still need to declare them in the configuration. At this point I started playing
around with nix
. I started up nix repl
and put together the following:
{ config, lib, pkgs, ... }:
let
node_name = "node0";
vpn_name = "example";
tcp_port = 655;
udp_port = 655;
ipv4_address = "10.0.0.1";
ipv4_prefix = 24;
files = builtins.readDir ("/etc/nixos/vpn/tinc/" + vpn_name);
filenames = builtins.attrNames files;
filepaths = map (x: "/etc/nixos/vpn/tinc/" + vpn_name + "/" + x) filenames;
filecontents = map builtins.readFile filepaths;
jsondata = map (x: builtins.fromJSON x) filecontents;
in
{
networking.firewall.allowedTCPPorts = [
tcp_port
];
networking.firewall.allowedUDPPorts = [
udp_port
];
networking.interfaces.("tinc." + vpn_name).ipv4.addresses = [
{
address = ipv4_address;
prefixLength = ipv4_prefix;
}
];
services.tinc.networks = {
mcrn0 = {
name = node_name;
hosts = let
attrsetdata = builtins.listToAttrs jsondata;
in attrsetdata;
};
};
}
At this point I can put all my VPN config files into (in this case) JSON format,
put them into a directory and fill out 6 values to set up the VPN:
$ tree /etc/nixos/vpn/tinc/example
/etc/nixos/vpn/tinc/example
βββ node0.json
βββ node1.json
βββ node2.json
$ bat /etc/nixos/vpn/tinc/example/node0.json
ββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β File: /etc/nixos/vpn/tinc/example/node0.json
ββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1 β {
2 β "name" : "node0",
3 β "value" : "Subnet = 10.0.0.1\nPort = 655\nEd25519PublicKey = ...\n-----BEGIN RSA PUBLIC KEY-----\n...\n-----END RSA PUBLIC KEY-----"
4 β }
ββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
So far so good, however Iβd like to manage more than one VPN and would prefer to
keep the functions at a single place instead of duplicating the nix
code from
above. As far as I understand this is where modules come in.
The next thing I looked at was how to write a module (especially writing one
that is using the <name>
feature was quite confusing to be honest). I played
around with a bunch of things, and finally ended up just taking the original
tinc
module and changing it up a bit. This is what Iβve ended up with so far
(since it is tinc
but Iβm trying to do things different, Iβve dubbed it
tincDifferent
) and so far it does not do too much:
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.tincDifferent;
in
{
options = {
services.tincDifferent = {
networks = mkOption {
default = { };
type = with types; attrsOf (submodule {
options = {
nodeName = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Name of the Node in the tinc network.
'';
};
tcpPort = mkOption {
default = 655;
type = types.int;
description = ''
TCP port of the tinc network.
'';
};
udpPort = mkOption {
default = 655;
type = types.int;
description = ''
UDP port of the tinc network.
'';
};
ipv4Address = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
IPv4 Address of the machine on the tinc network.
'';
example = "10.0.0.1";
};
ipv4Prefix = mkOption {
default = null;
type = types.nullOr types.int;
description = ''
IPv4 Prefix of the machine on the tinc network.
'';
example = 24;
};
};
});
description = ''
Defines the tinc networks which will be started.
Each network invokes a different daemon.
'';
};
};
};
config = {
networking.firewall = fold (a: b: a // b) { }
(flip mapAttrsToList cfg.networks (network: data:
{
allowedTCPPorts = [ data.tcpPort ];
allowedUDPPorts = [ data.udpPort ];
}
));
networking.interfaces = fold (a: b: a // b) { }
(flip mapAttrsToList cfg.networks (network: data:
{
"tinc.${network}".ipv4.addresses = [
{
address = data.ipv4Address;
prefixLength = data.ipv4Prefix;
}
];
}
));
};
}
Iβve basically introduced a few variables and for now setting the TCP and UDP
ports as well as the Interface seems to work, Iβm basically using the fold
and
flip
code from the original tinc
module to grab the network name as well as
the data configured for the network.
I can now use something like this to set up a network interface and open ports:
{ config, lib, pkgs, ... }:
{
imports = [ ./modules/tincDifferent.nix ];
services.tincDifferent.networks.example.nodeName = "node0";
services.tincDifferent.networks.example.ipv4Address = "10.0.0.1";
services.tincDifferent.networks.example.ipv4Prefix = 24;
}
What Iβm really struggling with is the part from my previous code where I loop
over the files inside the directory and set up the nodes one by one. This is the
part where Iβm canβt seem to make any progressβ¦ guess Iβve reached my limit of
what is possible with a bit of cargo culting and now Iβm standing on top of my
fake runway wearing fake Headphones and waving sticks in the air, hoping for
some mighty being to drop me a bunch of nix
expressions from the enigmatic
heavens.
So far this is what Iβve got, obviously it does not work and I canβt seem to
fold
or flip
my head around it:
services.tinc.networks = fold (a: b: a // b) { }
(flip mapAttrsToList cfg.networks (network: data:
{
${network} = {
name = data.nodeName;
};
}
));
Before ending this cry for help, I would like to describe the rest of whatβs on
my mind regarding this little VPN project, there are basically two additional
goals and so far Iβm not sure I can tackle them by myself:
- I would like to have the ability to set up routes on a per host base.
tinc
itself is able to do so. Here Iβm not sure which option I have to use, there
is an easy way to set up routes on a per interface basis, but Iβd set the
route up by supplying a next hop, so I guess Iβll have to use these options
somehow:
networking.interfaces.<name>.ipv4.routes.*.via = ?
networking.interfaces.<name>.ipv4.routes.*.prefixLength = ?
Here Iβm not sure, on the commandline I just set the routes up like this:
$ ip route add 192.168.0.0/24 via 10.0.0.1
- Secondly Iβd like to stick to keeping a single configuration file per host.
In my code above Iβve usedJSON
to store the name and config of the hosts
and then usedbuiltins.listToAttrs
andbuiltins.fromJSON
in order to
first get a list and then get a attrset. Iβve tried writing somenix
code
so I can configure all of my hosts with.nix
files, however trying to write
something up didnβt really turn out to be very fruitful. Ideally Iβd like to
have a way of defining additional configuration on a per hosts base and from
what is my understanding using plain.nix
files would enable me to directly
define stuff such as routes and at the same time take out information like
the node name and its configuration in form of an attrset and feed it to the
function that sets uptinc
. I hope that makes any senseβ¦
Thanks for reading this long text! Any help I can get is really appreciated