2023-07-15 14:54:42 +00:00
|
|
|
{ lib, pkgs, config, nodes, ... }:
|
2023-01-25 11:48:44 +00:00
|
|
|
let
|
|
|
|
cfg = config.skynet_dns;
|
2023-04-23 12:37:42 +00:00
|
|
|
|
2023-04-29 01:35:58 +00:00
|
|
|
# reads that date to a string (will need to be fixed in 2038)
|
2023-07-25 07:17:38 +00:00
|
|
|
current_date = lib.readFile "${pkgs.runCommand "timestamp" {} "echo -n `date +%s` > $out"}";
|
2023-04-29 00:54:17 +00:00
|
|
|
|
2023-07-16 00:53:21 +00:00
|
|
|
# gets a list of records that match this type
|
|
|
|
filter_records_type = r_type: builtins.filter (x: x.r_type == r_type) records;
|
|
|
|
filter_records_server = builtins.filter (x: builtins.hasAttr "server" x && x.server) (filter_records_type "A");
|
|
|
|
filter_records_a = builtins.filter (x: builtins.hasAttr "server" x && !x.server) (filter_records_type "A");
|
|
|
|
|
|
|
|
process_ptr = records: lib.lists.forEach records (x: process_ptr_sub x);
|
|
|
|
process_ptr_sub = record: {record=(builtins.substring 9 3 record.record); r_type="PTR"; value=record.value;};
|
|
|
|
ip_ptr_to_int = ip: lib.strings.toInt (builtins.substring 9 3 ip);
|
|
|
|
|
|
|
|
sort_records_server = builtins.sort (a: b: a.record < b.record) filter_records_server;
|
|
|
|
sort_records_a = builtins.sort (a: b: (ip_ptr_to_int a.value) < (ip_ptr_to_int b.value)) filter_records_a;
|
|
|
|
sort_records_cname = builtins.sort (a: b: a.value < b.value) (filter_records_type "CNAME");
|
|
|
|
sort_records_ptr = builtins.sort (a: b: (lib.strings.toInt a.record) < (lib.strings.toInt b.record)) (process_ptr (filter_records_type "PTR"));
|
2023-07-24 16:06:42 +00:00
|
|
|
sort_records_srv = builtins.sort (a: b: a.record < b.record) (filter_records_type "SRV");
|
2023-07-16 00:53:21 +00:00
|
|
|
|
|
|
|
format_records = records: offset: lib.strings.concatMapStrings (x: "${padString x.record offset} IN ${padString x.r_type 5} ${x.value}\n") records;
|
|
|
|
|
|
|
|
# small function to trim it down a tad
|
|
|
|
padString = text: length: fixedWidthString_post length " " text;
|
|
|
|
|
|
|
|
# like lib.strings.fixedWidthString but postfix
|
|
|
|
fixedWidthString_post = width: filler: str:
|
|
|
|
let
|
|
|
|
strw = lib.stringLength str;
|
|
|
|
reqWidth = width - (lib.stringLength filler);
|
|
|
|
in
|
|
|
|
assert lib.assertMsg (strw <= width) "fixedWidthString_post: requested string length (${toString width}) must not be shorter than actual length (${toString strw})";
|
|
|
|
if strw == width
|
|
|
|
then str
|
|
|
|
else (fixedWidthString_post reqWidth filler str) + filler;
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-05-21 19:22:54 +00:00
|
|
|
# base config for domains we own (skynet.ie, csn.ul.ie, ulcompsoc.ie)
|
2023-05-21 15:18:39 +00:00
|
|
|
get_config_file = (domain:
|
2023-07-08 09:27:30 +00:00
|
|
|
''$TTL 60 ; 1 minute
|
|
|
|
; hostmaster@${domain} is an email address that recieves stuff related to dns
|
2023-07-15 14:54:42 +00:00
|
|
|
@ IN SOA ${nameserver}.${domain}. hostmaster.${domain}. (
|
2023-07-08 09:27:30 +00:00
|
|
|
; Serial (YYYYMMDDCC) this has to be updated for each time the record is updated
|
|
|
|
${current_date}
|
|
|
|
600 ; Refresh (10 minutes)
|
|
|
|
300 ; Retry (5 minutes)
|
|
|
|
604800 ; Expire (1 week)
|
|
|
|
3600 ; Minimum (1 hour)
|
|
|
|
)
|
2023-05-21 15:18:39 +00:00
|
|
|
|
2023-07-08 09:27:30 +00:00
|
|
|
@ NS ns1.${domain}.
|
|
|
|
@ NS ns2.${domain}.
|
|
|
|
; @ stands for teh root domain so teh A record below is where ${domain} points to
|
|
|
|
;@ A 193.1.99.76
|
|
|
|
;@ MX 5 ${domain}.
|
2023-05-21 15:18:39 +00:00
|
|
|
|
2023-07-08 09:27:30 +00:00
|
|
|
; can have multiple mailserves
|
|
|
|
@ MX 10 mail.${domain}.
|
2023-05-21 15:18:39 +00:00
|
|
|
|
|
|
|
|
2023-07-08 09:27:30 +00:00
|
|
|
; ------------------------------------------
|
2023-07-16 00:53:21 +00:00
|
|
|
; Server Names (A Records)
|
2023-07-08 09:27:30 +00:00
|
|
|
; ------------------------------------------
|
2023-07-16 00:53:21 +00:00
|
|
|
${format_records sort_records_server 11}
|
2023-05-21 15:18:39 +00:00
|
|
|
|
2023-07-08 09:27:30 +00:00
|
|
|
; ------------------------------------------
|
2023-07-16 00:53:21 +00:00
|
|
|
; A (non server names
|
|
|
|
; ------------------------------------------
|
|
|
|
${format_records sort_records_a 18}
|
2023-05-21 15:18:39 +00:00
|
|
|
|
2023-07-16 00:53:21 +00:00
|
|
|
; ------------------------------------------
|
|
|
|
; CNAMES
|
|
|
|
; ------------------------------------------
|
|
|
|
${format_records sort_records_cname 31}
|
2023-05-21 15:18:39 +00:00
|
|
|
|
2023-07-08 09:27:30 +00:00
|
|
|
; ------------------------------------------
|
2023-07-16 00:53:21 +00:00
|
|
|
; TXT
|
|
|
|
; ------------------------------------------
|
|
|
|
${format_records (filter_records_type "TXT") 29}
|
2023-05-21 15:18:39 +00:00
|
|
|
|
2023-07-24 14:52:05 +00:00
|
|
|
; ------------------------------------------
|
|
|
|
; SRV
|
|
|
|
; ------------------------------------------
|
2023-07-24 20:15:54 +00:00
|
|
|
${format_records sort_records_srv 17}
|
2023-07-24 14:52:05 +00:00
|
|
|
|
|
|
|
|
2023-07-08 09:27:30 +00:00
|
|
|
''
|
2023-05-21 15:18:39 +00:00
|
|
|
);
|
|
|
|
|
2023-05-21 18:06:30 +00:00
|
|
|
|
|
|
|
# https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/4/html/reference_guide/s2-bind-configuration-zone-reverse
|
2023-05-21 19:22:54 +00:00
|
|
|
# config for our reverse dnspointers (not properly working)
|
2023-05-21 18:06:30 +00:00
|
|
|
get_config_file_rev = (domain:
|
2023-08-09 17:34:24 +00:00
|
|
|
''$ORIGIN 64-64.99.1.193.in-addr.arpa.
|
2023-08-06 01:48:15 +00:00
|
|
|
$TTL 60 ; 1 minute
|
2023-07-08 09:27:30 +00:00
|
|
|
; hostmaster@skynet.ie is an email address that recieves stuff related to dns
|
2023-07-15 14:54:42 +00:00
|
|
|
@ IN SOA ${nameserver}.skynet.ie. hostmaster.skynet.ie. (
|
2023-07-08 09:27:30 +00:00
|
|
|
; Serial (YYYYMMDDCC) this has to be updated for each time the record is updated
|
|
|
|
${current_date}
|
|
|
|
600 ; Refresh (10 minutes)
|
|
|
|
300 ; Retry (5 minutes)
|
|
|
|
604800 ; Expire (1 week)
|
|
|
|
3600 ; Minimum (1 hour)
|
|
|
|
)
|
|
|
|
|
|
|
|
@ NS ns1.skynet.ie.
|
|
|
|
@ NS ns2.skynet.ie.
|
|
|
|
|
2023-07-16 00:53:21 +00:00
|
|
|
; ------------------------------------------
|
|
|
|
; PTR
|
|
|
|
; ------------------------------------------
|
|
|
|
${format_records sort_records_ptr 3}
|
2023-07-08 09:27:30 +00:00
|
|
|
''
|
2023-05-21 18:06:30 +00:00
|
|
|
);
|
|
|
|
|
2023-05-21 19:22:54 +00:00
|
|
|
# domains we dont have proper ownship over, only here to ensure the logs dont get cluttered.
|
2023-05-21 18:06:30 +00:00
|
|
|
get_config_file_old_domains = (domain:
|
2023-07-08 09:27:30 +00:00
|
|
|
''$TTL 60 ; 1 minute
|
|
|
|
; hostmaster@skynet.ie is an email address that recieves stuff related to dns
|
2023-07-15 14:54:42 +00:00
|
|
|
@ IN SOA ${nameserver}.skynet.ie. hostmaster.skynet.ie. (
|
2023-07-08 09:27:30 +00:00
|
|
|
; Serial (YYYYMMDDCC) this has to be updated for each time the record is updated
|
|
|
|
${current_date}
|
|
|
|
600 ; Refresh (10 minutes)
|
|
|
|
300 ; Retry (5 minutes)
|
|
|
|
604800 ; Expire (1 week)
|
|
|
|
3600 ; Minimum (1 hour)
|
|
|
|
)
|
|
|
|
|
|
|
|
@ NS ns1.skynet.ie.
|
|
|
|
@ NS ns2.skynet.ie.
|
|
|
|
|
|
|
|
''
|
2023-05-21 18:06:30 +00:00
|
|
|
);
|
|
|
|
|
2023-05-21 18:38:13 +00:00
|
|
|
# arrys of teh two nameservers
|
|
|
|
tmp1 = ["193.1.99.109"];
|
|
|
|
tmp2 = ["193.1.99.120"];
|
|
|
|
|
2023-07-15 14:54:42 +00:00
|
|
|
primaries = (if cfg.server.primary then
|
2023-05-21 18:38:13 +00:00
|
|
|
# primary servers have no primaries (ones they listen to)
|
|
|
|
[]
|
|
|
|
else
|
2023-07-15 14:54:42 +00:00
|
|
|
if builtins.elem cfg.server.ip tmp1 then
|
2023-05-21 18:38:13 +00:00
|
|
|
tmp2
|
|
|
|
else
|
|
|
|
tmp1
|
|
|
|
);
|
|
|
|
|
2023-07-15 14:54:42 +00:00
|
|
|
secondaries = (if cfg.server.primary then
|
|
|
|
if builtins.elem cfg.server.ip tmp1 then
|
2023-05-21 18:38:13 +00:00
|
|
|
tmp2
|
|
|
|
else
|
|
|
|
tmp1
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
|
2023-05-21 19:22:54 +00:00
|
|
|
# small function to tidy up the spam of the cache networks, would use teh subnet except all external traffic has the ip of teh router
|
|
|
|
create_cache_networks = (map (x: "193.1.99.${toString x}/32" ) (lib.lists.range 71 126) );
|
2023-05-21 18:38:13 +00:00
|
|
|
|
2023-05-21 20:25:21 +00:00
|
|
|
|
2023-05-21 20:30:14 +00:00
|
|
|
# standard function to create the etc file, pass in the text and domain and it makes it
|
|
|
|
create_entry_etc_sub = domain: text: {
|
|
|
|
# Creates /etc/skynet/dns/domain
|
2023-05-21 20:25:21 +00:00
|
|
|
"skynet/dns/${domain}" = {
|
|
|
|
user = "named";
|
|
|
|
group = "named";
|
|
|
|
|
|
|
|
# The UNIX file mode bits
|
2023-05-21 20:51:17 +00:00
|
|
|
mode = "0664";
|
2023-05-21 20:25:21 +00:00
|
|
|
|
|
|
|
text = text;
|
|
|
|
};
|
|
|
|
};
|
2023-05-21 20:30:14 +00:00
|
|
|
# (text.owned "csn.ul.ie")
|
|
|
|
|
|
|
|
|
|
|
|
# standard function to create the etc file, pass in the text and domain and it makes it
|
|
|
|
create_entry_etc = domain: type:
|
|
|
|
if type == "owned" then
|
2023-05-21 20:48:30 +00:00
|
|
|
create_entry_etc_sub domain (text.owned domain)
|
|
|
|
else if type == "reverse" then
|
|
|
|
create_entry_etc_sub domain (text.reverse domain)
|
|
|
|
else if type == "old" then
|
|
|
|
create_entry_etc_sub domain (text.old domain)
|
2023-05-21 20:30:14 +00:00
|
|
|
else
|
2023-05-21 20:48:30 +00:00
|
|
|
{};
|
2023-05-21 20:25:21 +00:00
|
|
|
|
|
|
|
create_entry_zone = (domain: extraConfig: {
|
|
|
|
"${domain}" = {
|
|
|
|
extraConfig = ''
|
2023-07-08 09:27:30 +00:00
|
|
|
${extraConfig}
|
|
|
|
// for bumping the config
|
|
|
|
// ${current_date}
|
|
|
|
'';
|
2023-05-21 20:25:21 +00:00
|
|
|
# really wish teh nixos config didnt use master/slave
|
2023-07-15 14:54:42 +00:00
|
|
|
master = cfg.server.primary;
|
2023-05-21 20:25:21 +00:00
|
|
|
masters = primaries;
|
|
|
|
slaves = secondaries;
|
|
|
|
# need to write this to a file
|
|
|
|
# using the date in it so it will trigger a restart
|
|
|
|
file = "/etc/skynet/dns/${domain}";
|
|
|
|
# no leading whitespace for first line
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
text = {
|
|
|
|
owned = domain: get_config_file domain;
|
2023-05-21 20:48:30 +00:00
|
|
|
reverse = domain: get_config_file_rev domain;
|
|
|
|
old = domain: get_config_file_old_domains domain;
|
2023-05-21 20:25:21 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
extraConfig = {
|
|
|
|
owned =
|
2023-07-15 14:54:42 +00:00
|
|
|
if cfg.server.primary then
|
2023-07-08 09:27:30 +00:00
|
|
|
''
|
|
|
|
allow-update { key rfc2136key.skynet.ie.; };
|
2023-05-21 20:25:21 +00:00
|
|
|
|
2023-07-08 09:27:30 +00:00
|
|
|
dnssec-policy default;
|
|
|
|
inline-signing yes;
|
|
|
|
''
|
2023-05-21 20:25:21 +00:00
|
|
|
else
|
|
|
|
"";
|
|
|
|
|
2023-05-21 20:48:30 +00:00
|
|
|
# no extra config for reverse
|
|
|
|
reverse = "";
|
|
|
|
|
|
|
|
old = "";
|
2023-05-21 20:25:21 +00:00
|
|
|
};
|
|
|
|
|
2023-07-16 00:53:21 +00:00
|
|
|
records = builtins.concatLists (
|
|
|
|
lib.attrsets.mapAttrsToList (key: value:
|
|
|
|
let
|
|
|
|
details_server = value.config.skynet_dns.server;
|
|
|
|
details_records = value.config.skynet_dns.records;
|
|
|
|
in
|
|
|
|
if builtins.hasAttr "skynet_dns" value.config
|
|
|
|
then (
|
|
|
|
# got to handle habing a dns record for the dns serves themselves.
|
|
|
|
if details_server.enable
|
2023-07-15 14:54:42 +00:00
|
|
|
then (
|
2023-07-16 00:53:21 +00:00
|
|
|
if details_server.primary
|
|
|
|
then details_records ++ [ {record="ns1"; r_type="A"; value=details_server.ip; server=false;} ]
|
|
|
|
else details_records ++ [ {record="ns2"; r_type="A"; value=details_server.ip; server=false;} ]
|
2023-07-15 14:54:42 +00:00
|
|
|
)
|
2023-07-16 00:53:21 +00:00
|
|
|
else details_records
|
|
|
|
)
|
|
|
|
else []
|
|
|
|
) nodes
|
|
|
|
);
|
2023-07-15 14:54:42 +00:00
|
|
|
|
|
|
|
nameserver = if cfg.server.primary then "ns1" else "ns2";
|
|
|
|
|
2023-01-25 11:48:44 +00:00
|
|
|
in {
|
2023-05-24 15:12:48 +00:00
|
|
|
|
|
|
|
imports = [
|
|
|
|
../applications/firewall.nix
|
|
|
|
];
|
|
|
|
|
2023-01-25 11:48:44 +00:00
|
|
|
options = {
|
|
|
|
skynet_dns = {
|
2023-07-15 14:54:42 +00:00
|
|
|
server = {
|
|
|
|
enable = lib.mkEnableOption {
|
|
|
|
default = false;
|
|
|
|
description = "Skynet DNS server";
|
|
|
|
type = lib.types.bool;
|
|
|
|
};
|
2023-01-25 11:48:44 +00:00
|
|
|
|
2023-07-15 14:54:42 +00:00
|
|
|
primary = lib.mkOption {
|
|
|
|
type = lib.types.bool;
|
|
|
|
default = false;
|
|
|
|
};
|
2023-05-21 18:38:13 +00:00
|
|
|
|
|
|
|
ip = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
description = ''
|
|
|
|
ip of this server
|
|
|
|
'';
|
|
|
|
};
|
2023-01-25 11:48:44 +00:00
|
|
|
};
|
|
|
|
|
2023-07-16 00:53:21 +00:00
|
|
|
records = lib.mkOption {
|
|
|
|
description = "Records, sorted based on therir type";
|
|
|
|
type = with lib.types; listOf (submodule {
|
|
|
|
options = {
|
|
|
|
record = lib.mkOption {
|
|
|
|
type = str;
|
|
|
|
};
|
|
|
|
r_type = lib.mkOption {
|
2023-07-24 14:52:05 +00:00
|
|
|
type = enum ["A" "CNAME" "TXT" "PTR" "SRV"];
|
2023-07-16 00:53:21 +00:00
|
|
|
};
|
|
|
|
value = lib.mkOption {
|
|
|
|
type = str;
|
|
|
|
};
|
|
|
|
server = lib.mkOption {
|
|
|
|
description = "Core record for a server";
|
|
|
|
type = bool;
|
|
|
|
default = false;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
2023-01-25 11:48:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-07-15 14:54:42 +00:00
|
|
|
config = lib.mkIf cfg.server.enable {
|
2023-05-21 20:48:30 +00:00
|
|
|
|
2023-05-24 15:12:48 +00:00
|
|
|
# open the firewall for this
|
|
|
|
skynet_firewall.forward = [
|
2023-07-15 14:54:42 +00:00
|
|
|
"ip daddr ${cfg.server.ip} tcp dport 53 counter packets 0 bytes 0 accept"
|
|
|
|
"ip daddr ${cfg.server.ip} udp dport 53 counter packets 0 bytes 0 accept"
|
2023-05-24 15:12:48 +00:00
|
|
|
];
|
|
|
|
|
2023-05-21 20:48:30 +00:00
|
|
|
services.bind.zones =
|
|
|
|
(create_entry_zone "csn.ul.ie" extraConfig.owned ) //
|
2023-06-15 21:50:29 +00:00
|
|
|
(create_entry_zone "skynet.ie" extraConfig.owned ) //
|
|
|
|
(create_entry_zone "ulcompsoc.ie" extraConfig.owned ) //
|
2023-05-21 20:48:30 +00:00
|
|
|
|
2023-08-09 17:34:24 +00:00
|
|
|
(create_entry_zone "64-64.99.1.193.in-addr.arpa" extraConfig.reverse ) //
|
2023-05-21 20:48:30 +00:00
|
|
|
|
|
|
|
(create_entry_zone "conradcollins.net" extraConfig.old )//
|
|
|
|
(create_entry_zone "edelharty.net" extraConfig.old );
|
|
|
|
|
|
|
|
environment.etc =
|
|
|
|
(create_entry_etc "csn.ul.ie" "owned") //
|
|
|
|
(create_entry_etc "skynet.ie" "owned") //
|
2023-06-15 21:50:29 +00:00
|
|
|
(create_entry_etc "ulcompsoc.ie" "owned") //
|
2023-05-21 20:48:30 +00:00
|
|
|
|
2023-08-09 17:34:24 +00:00
|
|
|
(create_entry_etc "64-64.99.1.193.in-addr.arpa" "reverse") //
|
2023-05-21 20:48:30 +00:00
|
|
|
|
|
|
|
(create_entry_etc "conradcollins.net" "old") //
|
|
|
|
(create_entry_etc "edelharty.net" "old");
|
|
|
|
|
|
|
|
|
2023-04-20 18:22:17 +00:00
|
|
|
# secrets required
|
2023-04-20 22:10:47 +00:00
|
|
|
age.secrets.dns_dnskeys = {
|
|
|
|
file = ../secrets/dns_dnskeys.conf.age;
|
|
|
|
owner = "named";
|
|
|
|
group = "named";
|
|
|
|
};
|
2023-04-20 18:22:17 +00:00
|
|
|
|
2023-04-20 23:53:25 +00:00
|
|
|
networking.firewall = {
|
|
|
|
allowedTCPPorts = [53];
|
|
|
|
allowedUDPPorts = [53];
|
|
|
|
};
|
|
|
|
|
2023-01-25 11:48:44 +00:00
|
|
|
services.bind = {
|
2023-04-23 03:22:01 +00:00
|
|
|
enable = true;
|
|
|
|
|
|
|
|
ipv4Only = true;
|
|
|
|
|
|
|
|
# need to take a look at https://nixos.org/manual/nixos/unstable/#module-security-acme-config-dns
|
|
|
|
extraConfig = ''
|
|
|
|
include "/run/agenix/dns_dnskeys";
|
|
|
|
'';
|
|
|
|
|
2023-04-24 19:17:24 +00:00
|
|
|
# piles of no valid RRSIG resolving 'com/DS/IN' errors
|
2023-04-25 14:11:02 +00:00
|
|
|
extraOptions = ''
|
|
|
|
dnssec-validation yes;
|
|
|
|
'';
|
2023-04-24 19:17:24 +00:00
|
|
|
|
2023-04-23 03:22:01 +00:00
|
|
|
# set the upstream dns servers
|
|
|
|
# overrides the default dns servers
|
|
|
|
forwarders = [
|
|
|
|
# Cloudflare
|
|
|
|
"1.1.1.1"
|
|
|
|
# Google
|
|
|
|
"8.8.8.8"
|
|
|
|
# Quad9
|
|
|
|
"9.9.9.9"
|
|
|
|
];
|
|
|
|
|
|
|
|
cacheNetworks = [
|
|
|
|
# this server itself
|
|
|
|
"127.0.0.0/24"
|
|
|
|
# all of skynet can use this as a resolver
|
2023-05-05 13:40:27 +00:00
|
|
|
/*
|
|
|
|
Origianl idea, however all external traffic had the ip of the router
|
2023-04-23 03:22:01 +00:00
|
|
|
"193.1.99.64/26"
|
2023-05-05 13:40:27 +00:00
|
|
|
|
|
|
|
So to fix this we need to allow smaller ranges? - Didnt work
|
|
|
|
Fallback is explisitly listing each ip we have
|
|
|
|
|
2023-05-21 19:22:54 +00:00
|
|
|
Now have a function for it
|
|
|
|
*/
|
|
|
|
] ++ create_cache_networks;
|
2023-04-23 03:22:01 +00:00
|
|
|
};
|
2023-01-25 11:48:44 +00:00
|
|
|
|
2023-04-23 03:22:01 +00:00
|
|
|
# creates a folder in /etc for the dns to use
|
|
|
|
users.users.named = {
|
|
|
|
createHome = true;
|
2023-05-21 20:25:21 +00:00
|
|
|
home = "/etc/skynet/dns";
|
2023-04-23 03:22:01 +00:00
|
|
|
};
|
2023-01-25 11:48:44 +00:00
|
|
|
|
2023-04-23 03:22:01 +00:00
|
|
|
};
|
2023-01-17 22:40:04 +00:00
|
|
|
}
|