Squashed commit of the following:
commitrelease014fa13262
Author: Tilmann Meyer <me@atiltedtree.dev> Date: Sun Mar 24 23:09:00 2024 +0100 Fixed persistent keepalive for networkd configurer commit37453982ab
Author: Matthew Salerno <m@salernosection.com> Date: Sun Mar 24 21:08:06 2024 -0400 update flake commit68a0496bc7
Author: Matthew Salerno <m@salernosection.com> Date: Sun Mar 24 21:07:59 2024 -0400 Update tests to use `subnets` in all connections commit8c7f741b7f
Author: Matthew Salerno <m@salernosection.com> Date: Sun Mar 24 21:07:31 2024 -0400 Add error message to parser for incorrectly configured subnets in connections commit3e3a37fc0f
Author: Matthew Salerno <m@salernosection.com> Date: Sun Mar 24 21:05:27 2024 -0400 Update disjoint test commit1236e4e8f2
Author: Adam Stephens <adam@valkor.net> Date: Sun Mar 24 20:59:26 2024 -0400 Add disjointed meshes tests commitdadd5bf720
Author: Matthew Salerno <m@salernosection.com> Date: Wed Dec 13 22:01:16 2023 -0500 add tags to agenix-rekey provider commit1d3184639a
Author: Matthew Salerno <m@salernosection.com> Date: Mon Dec 11 22:09:45 2023 -0500 sneaky systemd issues with dev only commitc8fb5affe5
Author: Matthew Salerno <m@salernosection.com> Date: Tue Dec 5 20:51:30 2023 -0500 wnlib is back commit1e697eb859
Author: Matthew Salerno <m@salernosection.com> Date: Sat Nov 11 22:09:26 2023 -0500 allow multiple devs for same subnet commit45b70c9063
Author: Matthew Salerno <m@salernosection.com> Date: Mon Sep 18 16:04:49 2023 -0400 added manual ip tests and resulting fixes commit3d49ebff29
Author: Matthew Salerno <m@salernosection.com> Date: Mon Sep 18 11:49:58 2023 -0400 Added manual IP assignment tests commitbd52d85d2d
Author: Matthew Salerno <m@salernosection.com> Date: Thu Sep 14 16:08:00 2023 -0400 Generalized ip assignment to take cidr or IP commitfd2b9ce77c
Author: Matthew Salerno <m@salernosection.com> Date: Thu Sep 14 13:50:11 2023 -0400 Generalized ip assignment to take cidr or IP commit57f8e0e974
Author: Matthew Salerno <m@salernosection.com> Date: Wed Sep 13 18:38:42 2023 -0400 Fixed manual ipv4 assignment issue commita24fffa753
Author: Matthew Salerno <m@salernosection.com> Date: Mon Sep 11 13:44:11 2023 -0400 Update README.md with link to self commitdd9de47a84
Author: Matthew Salerno <m@salernosection.com> Date: Sat Sep 2 19:55:17 2023 -0400 fixed missing link commite761330e91
Merge:bb8636d
b658653
Author: Matthew Salerno <m@salernosection.com> Date: Sat Sep 2 19:54:29 2023 -0400 fixed missing link commitbb8636dd8d
Author: Matthew Salerno <m@salernosection.com> Date: Thu Aug 31 20:45:14 2023 -0400 Readme moved to wiki commit86e300428b
Author: Matthew Salerno <m@salernosection.com> Date: Tue Aug 22 21:07:34 2023 -0400 Fixed a bug in allGroupEndpoints logic commit9a5c773355
Author: Matthew Salerno <m@salernosection.com> Date: Mon Aug 21 22:03:19 2023 -0400 oops, delete net.nix commitb97760e456
Author: Matthew Salerno <m@salernosection.com> Date: Mon Aug 21 21:59:30 2023 -0400 fixup additional_ settings commit753c072663
Author: Matthew Salerno <m@salernosection.com> Date: Mon Aug 21 21:30:40 2023 -0400 change null test to test enable = false commit690e13e902
Author: Matthew Salerno <m@salernosection.com> Date: Mon Aug 21 21:28:29 2023 -0400 add mailing list to readme
parent
0dea96cf37
commit
c1e3bf1800
@ -0,0 +1 @@
|
|||||||
|
/result
|
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
{lib, devNameMethod ? "short", ...}@inputs: keyProviders: intermediateConfig: localPeerName:
|
||||||
|
let wnlib = import ../lib.nix {inherit lib;}; in
|
||||||
|
with wnlib;
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
thisPeer = intermediateConfig.peers."${localPeerName}";
|
||||||
|
# these aren't really important, I just wanted to reverse the argument order
|
||||||
|
forEachAttr' = flip mapAttrs';
|
||||||
|
forEachAttrToList = flip mapAttrsToList;
|
||||||
|
devName = getDevName devNameMethod localPeerName;
|
||||||
|
in
|
||||||
|
with getKeyProviderFuncs keyProviders inputs intermediateConfig localPeerName;
|
||||||
|
{
|
||||||
|
networking.hosts = foldl' (mergeAttrs) {} (concatLists ( concatLists (forEachAttrToList thisPeer.subnetConnections (subnetName: subnetConnection:
|
||||||
|
forEachAttrToList subnetConnection.peerConnections (remotePeerName: peerConnection: forEach peerConnection.ipAddresses (ip: {"${asIp ip}" = ["${remotePeerName}.${subnetName}"];}))
|
||||||
|
))));
|
||||||
|
systemd.network = {
|
||||||
|
netdevs = forEachAttr' thisPeer.subnetConnections (subnetName: subnetConnection: nameValuePair "50-${devName subnetName}" {
|
||||||
|
netdevConfig = {
|
||||||
|
Kind = "wireguard";
|
||||||
|
Name = "${devName subnetName}";
|
||||||
|
};
|
||||||
|
wireguardConfig = {
|
||||||
|
ListenPort = subnetConnection.listenPort;
|
||||||
|
PrivateKeyFile = getPrivKeyFile;
|
||||||
|
};
|
||||||
|
wireguardPeers = forEachAttrToList subnetConnection.peerConnections (remotePeerName: peerConnection: {
|
||||||
|
wireguardPeerConfig = {
|
||||||
|
Endpoint = "${peerConnection.endpoint.ip}:${builtins.toString peerConnection.endpoint.port}";
|
||||||
|
PublicKey = getPeerPubKey remotePeerName;
|
||||||
|
AllowedIPs = map (ip: asCidr ip) peerConnection.ipAddresses;
|
||||||
|
PresharedKeyFile = getSubnetPSKFile subnetName;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// (if peerConnection.endpoint ? persistentKeepalive then {PersistentKeepalive = peerConnection.endpoint.persistentKeepalive;} else {})
|
||||||
|
// (warnIf (peerConnection.endpoint ? dynamicEndpointRefreshSeconds) "dynamicEndpointRefreshSeconds not supported for networkd" {})
|
||||||
|
// (warnIf (peerConnection.endpoint ? dynamicEndpointRefreshRestartSeconds) "dynamicEndpointRefreshRestartSeconds not supported for networkd" {})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} // getProviderConfig
|
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
version = "v1";
|
||||||
|
subnets = [
|
||||||
|
{
|
||||||
|
name = "disjoint1";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# No match mean match any
|
||||||
|
port = 51820;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "disjoint2";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# No match mean match any
|
||||||
|
port = 51821;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
groups = [
|
||||||
|
# groups field is expected, but can be empty
|
||||||
|
];
|
||||||
|
peers = [
|
||||||
|
{
|
||||||
|
name = "node1";
|
||||||
|
subnets = {
|
||||||
|
disjoint1 = {
|
||||||
|
listenPort = 51820;
|
||||||
|
# empty ipAddresses will auto generate an IPv6 address
|
||||||
|
};
|
||||||
|
};
|
||||||
|
publicKey = "kdyzqV8cBQtDYeW6R1vUug0Oe+KaytHHDS7JoCp/kTE=";
|
||||||
|
privateKeyFile = "/etc/wg-key";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# no match can be any
|
||||||
|
ip = "node1";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "node2";
|
||||||
|
subnets = {
|
||||||
|
disjoint1 = {
|
||||||
|
listenPort = 51820;
|
||||||
|
};
|
||||||
|
disjoint2 = {
|
||||||
|
listenPort = 51821;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
publicKey = "ztdAXTspQEZUNpxUbUdAhhRWbiL3YYWKSK0ZGdcsMHE=";
|
||||||
|
privateKeyFile = "/etc/wg-key";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# no match can be any
|
||||||
|
ip = "node2";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "node3";
|
||||||
|
subnets = {
|
||||||
|
disjoint2 = {
|
||||||
|
listenPort = 51821;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
publicKey = "VR5SILc/2MkWSeGOVAJ/0Ru5H4DFheNvNUiT0fPtgiI=";
|
||||||
|
privateKeyFile = "/etc/wg-key";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# no match can be any
|
||||||
|
ip = "node3";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
connections = [
|
||||||
|
{
|
||||||
|
a = [{type= "subnet"; rule = "is"; value = "disjoint1";}];
|
||||||
|
b = [{type= "subnet"; rule = "is"; value = "disjoint1";}];
|
||||||
|
subnets = ["disjoint1"];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
a = [{type= "subnet"; rule = "is"; value = "disjoint2";}];
|
||||||
|
b = [{type= "subnet"; rule = "is"; value = "disjoint2";}];
|
||||||
|
subnets = ["disjoint2"];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
version = "v1";
|
||||||
|
subnets = [
|
||||||
|
{
|
||||||
|
name = "ring";
|
||||||
|
endpoints = [
|
||||||
|
{}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
groups = [
|
||||||
|
# groups field is expected, but can be empty
|
||||||
|
];
|
||||||
|
peers = [
|
||||||
|
{
|
||||||
|
name = "peer1";
|
||||||
|
subnets = {
|
||||||
|
ring = {
|
||||||
|
listenPort = 51820;
|
||||||
|
# empty ipAddresses will auto generate an IPv6 address
|
||||||
|
};
|
||||||
|
};
|
||||||
|
publicKey = "kdyzqV8cBQtDYeW6R1vUug0Oe+KaytHHDS7JoCp/kTE=";
|
||||||
|
privateKeyFile = "/etc/wg-key1";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# no match can be any
|
||||||
|
ip = "node1";
|
||||||
|
port = 51820;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "peer2";
|
||||||
|
subnets = {
|
||||||
|
ring = {
|
||||||
|
listenPort = 51820;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
publicKey = "ztdAXTspQEZUNpxUbUdAhhRWbiL3YYWKSK0ZGdcsMHE=";
|
||||||
|
privateKeyFile = "/etc/wg-key2";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# no match can be any
|
||||||
|
ip = "node2";
|
||||||
|
port = 51820;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "peer3";
|
||||||
|
subnets = {
|
||||||
|
ring = {
|
||||||
|
listenPort = 51821;
|
||||||
|
# empty ipAddresses will auto generate an IPv6 address
|
||||||
|
};
|
||||||
|
};
|
||||||
|
publicKey = "43tP6JgckdTFrnbYuy8a42jdNt3+wwVcb4+ae5U4ez4=";
|
||||||
|
privateKeyFile = "/etc/wg-key3";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# no match can be any
|
||||||
|
ip = "node1";
|
||||||
|
port = 51821;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "peer4";
|
||||||
|
subnets = {
|
||||||
|
ring = {
|
||||||
|
listenPort = 51821;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
publicKey = "g6+Tq9aeVfm5CXPIwZDqoTxGmsQ/TlLtxcxVn2aSiVA=";
|
||||||
|
privateKeyFile = "/etc/wg-key4";
|
||||||
|
endpoints = [
|
||||||
|
{
|
||||||
|
# no match can be any
|
||||||
|
ip = "node2";
|
||||||
|
port = 51821;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
connections = [
|
||||||
|
{
|
||||||
|
a = [{type= "peer"; rule = "is"; value = "peer1";}];
|
||||||
|
b = [{type= "peer"; rule = "is"; value = "peer2";}];
|
||||||
|
subnets = [ "ring" ];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
a = [{type= "peer"; rule = "is"; value = "peer2";}];
|
||||||
|
b = [{type= "peer"; rule = "is"; value = "peer3";}];
|
||||||
|
subnets = [ "ring" ];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
a = [{type= "peer"; rule = "is"; value = "peer3";}];
|
||||||
|
b = [{type= "peer"; rule = "is"; value = "peer4";}];
|
||||||
|
subnets = [ "ring" ];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
a = [{type= "peer"; rule = "is"; value = "peer4";}];
|
||||||
|
b = [{type= "peer"; rule = "is"; value = "peer1";}];
|
||||||
|
subnets = [ "ring" ];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
(import ./lib.nix) ({wnlib}:
|
||||||
|
{
|
||||||
|
name = "disjointed-meshes connection";
|
||||||
|
nodes = {
|
||||||
|
# `self` here is set by using specialArgs in `lib.nix`
|
||||||
|
node1 = { self, pkgs, ... }: {
|
||||||
|
virtualisation.vlans = [ 1 ];
|
||||||
|
imports = [ self.nixosModules.default ];
|
||||||
|
wirenix = {
|
||||||
|
enable = true;
|
||||||
|
keyProviders = ["acl"];
|
||||||
|
peerName = "node1";
|
||||||
|
aclConfig = import ./acls/disjointed-meshes.nix;
|
||||||
|
};
|
||||||
|
# Don't do this! This is for testing only!
|
||||||
|
environment.etc."wg-key" = {
|
||||||
|
text = "MIELhEc0I7BseAanhk/+LlY/+Yf7GK232vKWITExnEI=";
|
||||||
|
};
|
||||||
|
networking.firewall.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
node2 = { self, pkgs, ... }: {
|
||||||
|
virtualisation.vlans = [ 1 ];
|
||||||
|
imports = [ self.nixosModules.default ];
|
||||||
|
wirenix = {
|
||||||
|
enable = true;
|
||||||
|
keyProviders = ["acl"];
|
||||||
|
peerName = "node2";
|
||||||
|
aclConfig = import ./acls/disjointed-meshes.nix;
|
||||||
|
};
|
||||||
|
environment.etc."wg-key" = {
|
||||||
|
text = "yG4mJiduoAvzhUJMslRbZwOp1gowSfC+wgY8B/Mul1M=";
|
||||||
|
};
|
||||||
|
networking.firewall.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
node3 = { self, pkgs, ... }: {
|
||||||
|
virtualisation.vlans = [ 1 ];
|
||||||
|
imports = [ self.nixosModules.default ];
|
||||||
|
wirenix = {
|
||||||
|
enable = true;
|
||||||
|
keyProviders = ["acl"];
|
||||||
|
peerName = "node3";
|
||||||
|
aclConfig = import ./acls/disjointed-meshes.nix;
|
||||||
|
};
|
||||||
|
environment.etc."wg-key" = {
|
||||||
|
text = "MFsj7nmb2efBFNwON8RxZf+MHbopTY9P3+/xhiqJFlM=";
|
||||||
|
};
|
||||||
|
networking.firewall.enable = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# This is the test code that will check if our service is running correctly:
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
node1.wait_for_unit("wireguard-disjoint1-peer-node2")
|
||||||
|
node2.wait_for_unit("wireguard-disjoint1-peer-node1")
|
||||||
|
node2.wait_for_unit("wireguard-disjoint2-peer-node3")
|
||||||
|
node3.wait_for_unit("wireguard-disjoint2-peer-node2")
|
||||||
|
|
||||||
|
node1.succeed("wg show >&2")
|
||||||
|
node2.succeed("wg show >&2")
|
||||||
|
node3.succeed("wg show >&2")
|
||||||
|
|
||||||
|
node1.succeed("ping -c 1 node2.disjoint1")
|
||||||
|
node1.fail("ping -c 1 node3.disjoint2")
|
||||||
|
|
||||||
|
node2.succeed("ping -c 1 node1.disjoint1")
|
||||||
|
node2.succeed("ping -c 1 node3.disjoint2")
|
||||||
|
|
||||||
|
node3.fail("ping -c 1 node1.disjoint1")
|
||||||
|
node3.succeed("ping -c 1 node2.disjoint2")
|
||||||
|
'';
|
||||||
|
})
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
(import ./lib.nix) ({wnlib}:
|
||||||
|
{
|
||||||
|
name = "double dev ring connection";
|
||||||
|
nodes = {
|
||||||
|
# `self` here is set by using specialArgs in `lib.nix`
|
||||||
|
node1 = { self, pkgs, ... }: {
|
||||||
|
virtualisation.vlans = [ 1 ];
|
||||||
|
imports = [ self.nixosModules.default ];
|
||||||
|
systemd.network.enable = true;
|
||||||
|
networking.useDHCP = false;
|
||||||
|
wirenix = {
|
||||||
|
configurer = "networkd";
|
||||||
|
devNameMethod = "hash";
|
||||||
|
enable = true;
|
||||||
|
aclConfig = import ./acls/double-dev-ring.nix;
|
||||||
|
peerNames = ["peer1" "peer3"];
|
||||||
|
};
|
||||||
|
environment.etc."wg-key1" = {
|
||||||
|
text = "MIELhEc0I7BseAanhk/+LlY/+Yf7GK232vKWITExnEI=";
|
||||||
|
};
|
||||||
|
environment.etc."wg-key3" = {
|
||||||
|
text = "yPcTvQOK9eVXQjLNapOsv2iAkbOeSzCCxlrWPMe1o0g=";
|
||||||
|
};
|
||||||
|
environment.systemPackages = [pkgs.wireguard-tools];
|
||||||
|
networking.firewall.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
node2 = { self, pkgs, ... }: {
|
||||||
|
virtualisation.vlans = [ 1 ];
|
||||||
|
imports = [ self.nixosModules.default ];
|
||||||
|
systemd.network.enable = true;
|
||||||
|
networking.useDHCP = false;
|
||||||
|
wirenix = {
|
||||||
|
configurer = "networkd";
|
||||||
|
devNameMethod = "hash";
|
||||||
|
enable = true;
|
||||||
|
keyProviders = ["acl"];
|
||||||
|
aclConfig = import ./acls/double-dev-ring.nix;
|
||||||
|
peerNames = ["peer2" "peer4"];
|
||||||
|
};
|
||||||
|
environment.etc."wg-key2" = {
|
||||||
|
text = "yG4mJiduoAvzhUJMslRbZwOp1gowSfC+wgY8B/Mul1M=";
|
||||||
|
};
|
||||||
|
environment.etc."wg-key4" = {
|
||||||
|
text = "CLREBQ+oGXsGxhlQc3ufSoBd7MNFoM6KmMnNyuQ9S0E=";
|
||||||
|
};
|
||||||
|
environment.systemPackages = [pkgs.wireguard-tools];
|
||||||
|
networking.firewall.enable = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# This is the test code that will check if our service is running correctly:
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
nodes = {
|
||||||
|
"peer1": node1,
|
||||||
|
"peer2": node2,
|
||||||
|
"peer3": node1,
|
||||||
|
"peer4": node2
|
||||||
|
}
|
||||||
|
ifaces = {
|
||||||
|
"peer1": "${wnlib.getDevName "hash" "peer1" "ring"}",
|
||||||
|
"peer2": "${wnlib.getDevName "hash" "peer2" "ring"}",
|
||||||
|
"peer3": "${wnlib.getDevName "hash" "peer3" "ring"}",
|
||||||
|
"peer4": "${wnlib.getDevName "hash" "peer4" "ring"}"
|
||||||
|
}
|
||||||
|
connections = {
|
||||||
|
"peer1": ["peer2", "peer4"],
|
||||||
|
"peer2": ["peer3", "peer1"],
|
||||||
|
"peer3": ["peer4", "peer2"],
|
||||||
|
"peer4": ["peer1", "peer3"]
|
||||||
|
}
|
||||||
|
node1.wait_for_unit("systemd-networkd-wait-online")
|
||||||
|
node2.wait_for_unit("systemd-networkd-wait-online")
|
||||||
|
node1.succeed("ping -c 3 node2 >&2")
|
||||||
|
node2.succeed("ping -c 3 node1 >&2")
|
||||||
|
for local_name, local_node in nodes.items():
|
||||||
|
for remote_name in set(nodes.keys()) - set([local_name]):
|
||||||
|
if remote_name in connections[local_name]:
|
||||||
|
local_node.succeed(f"ping -c 3 -I {ifaces[local_name]} {remote_name}.ring >&2")
|
||||||
|
else:
|
||||||
|
local_node.fail(f"ping -c 3 -W 1 -I {ifaces[local_name]} {remote_name}.ring")
|
||||||
|
'';
|
||||||
|
})
|
Loading…
Reference in New Issue