dns: auto-generate zone entries from host configurations
Replace static zone file with dynamically generated records: - Add homelab.dns module with enable/cnames options - Extract IPs from systemd.network configs (filters VPN interfaces) - Use git commit timestamp as zone serial number - Move external hosts to separate external-hosts.nix Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@
|
|||||||
../../common/vm
|
../../common/vm
|
||||||
];
|
];
|
||||||
|
|
||||||
|
homelab.dns.cnames = [ "ldap" ];
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
# Use the systemd-boot EFI boot loader.
|
# Use the systemd-boot EFI boot loader.
|
||||||
boot.loader.grub = {
|
boot.loader.grub = {
|
||||||
|
|||||||
@@ -11,6 +11,22 @@
|
|||||||
../../common/vm
|
../../common/vm
|
||||||
];
|
];
|
||||||
|
|
||||||
|
homelab.dns.cnames = [
|
||||||
|
"nzbget"
|
||||||
|
"radarr"
|
||||||
|
"sonarr"
|
||||||
|
"ha"
|
||||||
|
"z2m"
|
||||||
|
"grafana"
|
||||||
|
"prometheus"
|
||||||
|
"alertmanager"
|
||||||
|
"jelly"
|
||||||
|
"auth"
|
||||||
|
"lldap"
|
||||||
|
"pyroscope"
|
||||||
|
"pushgw"
|
||||||
|
];
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
# Use the systemd-boot EFI boot loader.
|
# Use the systemd-boot EFI boot loader.
|
||||||
boot.loader.grub = {
|
boot.loader.grub = {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
../../common/vm
|
../../common/vm
|
||||||
];
|
];
|
||||||
|
|
||||||
|
homelab.dns.cnames = [ "nix-cache" "actions1" ];
|
||||||
|
|
||||||
fileSystems."/nix" = {
|
fileSystems."/nix" = {
|
||||||
device = "/dev/disk/by-label/nixcache";
|
device = "/dev/disk/by-label/nixcache";
|
||||||
fsType = "xfs";
|
fsType = "xfs";
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
../../system
|
../../system
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Template host - exclude from DNS zone generation
|
||||||
|
homelab.dns.enable = false;
|
||||||
|
|
||||||
|
|
||||||
boot.loader.grub.enable = true;
|
boot.loader.grub.enable = true;
|
||||||
boot.loader.grub.device = "/dev/sda";
|
boot.loader.grub.device = "/dev/sda";
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
../../common/vm
|
../../common/vm
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Test VM - exclude from DNS zone generation
|
||||||
|
homelab.dns.enable = false;
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
boot.loader.grub.enable = true;
|
boot.loader.grub.enable = true;
|
||||||
boot.loader.grub.device = "/dev/vda";
|
boot.loader.grub.device = "/dev/vda";
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
../../services/vault
|
../../services/vault
|
||||||
];
|
];
|
||||||
|
|
||||||
|
homelab.dns.cnames = [ "vault" ];
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
boot.loader.grub.enable = true;
|
boot.loader.grub.enable = true;
|
||||||
boot.loader.grub.device = "/dev/vda";
|
boot.loader.grub.device = "/dev/vda";
|
||||||
|
|||||||
160
lib/dns-zone.nix
Normal file
160
lib/dns-zone.nix
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
{ lib }:
|
||||||
|
let
|
||||||
|
# Pad string on the right to reach a fixed width
|
||||||
|
rightPad = width: str:
|
||||||
|
let
|
||||||
|
len = builtins.stringLength str;
|
||||||
|
padding = if len >= width then "" else lib.strings.replicate (width - len) " ";
|
||||||
|
in
|
||||||
|
str + padding;
|
||||||
|
|
||||||
|
# Extract IP address from CIDR notation (e.g., "10.69.13.5/24" -> "10.69.13.5")
|
||||||
|
extractIP = address:
|
||||||
|
let
|
||||||
|
parts = lib.splitString "/" address;
|
||||||
|
in
|
||||||
|
builtins.head parts;
|
||||||
|
|
||||||
|
# Check if a network interface name looks like a VPN/tunnel interface
|
||||||
|
isVpnInterface = ifaceName:
|
||||||
|
lib.hasPrefix "wg" ifaceName ||
|
||||||
|
lib.hasPrefix "tun" ifaceName ||
|
||||||
|
lib.hasPrefix "tap" ifaceName ||
|
||||||
|
lib.hasPrefix "vti" ifaceName;
|
||||||
|
|
||||||
|
# Extract DNS information from a single host configuration
|
||||||
|
# Returns null if host should not be included in DNS
|
||||||
|
extractHostDNS = name: hostConfig:
|
||||||
|
let
|
||||||
|
cfg = hostConfig.config;
|
||||||
|
# Handle cases where homelab module might not be imported
|
||||||
|
dnsConfig = (cfg.homelab or { }).dns or { enable = true; cnames = [ ]; };
|
||||||
|
hostname = cfg.networking.hostName;
|
||||||
|
networks = cfg.systemd.network.networks or { };
|
||||||
|
|
||||||
|
# Filter out VPN interfaces and find networks with static addresses
|
||||||
|
# Check matchConfig.Name instead of network unit name (which can have prefixes like "40-")
|
||||||
|
physicalNetworks = lib.filterAttrs
|
||||||
|
(netName: netCfg:
|
||||||
|
let
|
||||||
|
ifaceName = netCfg.matchConfig.Name or "";
|
||||||
|
in
|
||||||
|
!(isVpnInterface ifaceName) && (netCfg.address or [ ]) != [ ])
|
||||||
|
networks;
|
||||||
|
|
||||||
|
# Get addresses from physical networks only
|
||||||
|
networkAddresses = lib.flatten (
|
||||||
|
lib.mapAttrsToList
|
||||||
|
(netName: netCfg: netCfg.address or [ ])
|
||||||
|
physicalNetworks
|
||||||
|
);
|
||||||
|
|
||||||
|
# Get the first address, if any
|
||||||
|
firstAddress = if networkAddresses != [ ] then builtins.head networkAddresses else null;
|
||||||
|
|
||||||
|
# Check if host uses DHCP (no static address)
|
||||||
|
usesDHCP = firstAddress == null ||
|
||||||
|
lib.any
|
||||||
|
(netName: (networks.${netName}.networkConfig.DHCP or "no") != "no")
|
||||||
|
(lib.attrNames networks);
|
||||||
|
in
|
||||||
|
if !(dnsConfig.enable or true) || firstAddress == null then
|
||||||
|
null
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inherit hostname;
|
||||||
|
ip = extractIP firstAddress;
|
||||||
|
cnames = dnsConfig.cnames or [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Generate A record line
|
||||||
|
generateARecord = hostname: ip:
|
||||||
|
"${rightPad 20 hostname}IN A ${ip}";
|
||||||
|
|
||||||
|
# Generate CNAME record line
|
||||||
|
generateCNAME = alias: target:
|
||||||
|
"${rightPad 20 alias}IN CNAME ${target}";
|
||||||
|
|
||||||
|
# Generate zone file from flake configurations and external hosts
|
||||||
|
generateZone =
|
||||||
|
{ self
|
||||||
|
, externalHosts
|
||||||
|
, serial
|
||||||
|
, domain ? "home.2rjus.net"
|
||||||
|
, ttl ? 1800
|
||||||
|
, refresh ? 3600
|
||||||
|
, retry ? 900
|
||||||
|
, expire ? 1209600
|
||||||
|
, minTtl ? 120
|
||||||
|
, nameservers ? [ "ns1" "ns2" "ns3" ]
|
||||||
|
, adminEmail ? "admin.test.2rjus.net"
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
# Extract DNS info from all flake hosts
|
||||||
|
nixosConfigs = self.nixosConfigurations or { };
|
||||||
|
hostDNSList = lib.filter (x: x != null) (
|
||||||
|
lib.mapAttrsToList extractHostDNS nixosConfigs
|
||||||
|
);
|
||||||
|
|
||||||
|
# Sort hosts by IP for consistent output
|
||||||
|
sortedHosts = lib.sort (a: b: a.ip < b.ip) hostDNSList;
|
||||||
|
|
||||||
|
# Generate A records for flake hosts
|
||||||
|
flakeARecords = lib.concatMapStringsSep "\n" (host:
|
||||||
|
generateARecord host.hostname host.ip
|
||||||
|
) sortedHosts;
|
||||||
|
|
||||||
|
# Generate CNAMEs for flake hosts
|
||||||
|
flakeCNAMEs = lib.concatMapStringsSep "\n" (host:
|
||||||
|
lib.concatMapStringsSep "\n" (cname:
|
||||||
|
generateCNAME cname host.hostname
|
||||||
|
) host.cnames
|
||||||
|
) (lib.filter (h: h.cnames != [ ]) sortedHosts);
|
||||||
|
|
||||||
|
# Generate A records for external hosts
|
||||||
|
externalARecords = lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (name: ip:
|
||||||
|
generateARecord name ip
|
||||||
|
) (externalHosts.aRecords or { })
|
||||||
|
);
|
||||||
|
|
||||||
|
# Generate CNAMEs for external hosts
|
||||||
|
externalCNAMEs = lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (alias: target:
|
||||||
|
generateCNAME alias target
|
||||||
|
) (externalHosts.cnames or { })
|
||||||
|
);
|
||||||
|
|
||||||
|
# NS records
|
||||||
|
nsRecords = lib.concatMapStringsSep "\n" (ns:
|
||||||
|
" IN NS ${ns}.${domain}."
|
||||||
|
) nameservers;
|
||||||
|
|
||||||
|
# SOA record
|
||||||
|
soa = ''
|
||||||
|
$ORIGIN ${domain}.
|
||||||
|
$TTL ${toString ttl}
|
||||||
|
@ IN SOA ns1.${domain}. ${adminEmail}. (
|
||||||
|
${toString serial} ; serial number
|
||||||
|
${toString refresh} ; refresh
|
||||||
|
${toString retry} ; retry
|
||||||
|
${toString expire} ; expire
|
||||||
|
${toString minTtl} ; ttl
|
||||||
|
)'';
|
||||||
|
in
|
||||||
|
lib.concatStringsSep "\n\n" (lib.filter (s: s != "") [
|
||||||
|
soa
|
||||||
|
nsRecords
|
||||||
|
"; Flake-managed hosts (auto-generated)"
|
||||||
|
flakeARecords
|
||||||
|
(if flakeCNAMEs != "" then "; Flake-managed CNAMEs\n${flakeCNAMEs}" else "")
|
||||||
|
"; External hosts (not managed by this flake)"
|
||||||
|
externalARecords
|
||||||
|
(if externalCNAMEs != "" then "; External CNAMEs\n${externalCNAMEs}" else "")
|
||||||
|
""
|
||||||
|
]);
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit extractIP extractHostDNS generateARecord generateCNAME generateZone;
|
||||||
|
}
|
||||||
6
modules/homelab/default.nix
Normal file
6
modules/homelab/default.nix
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./dns.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
20
modules/homelab/dns.nix
Normal file
20
modules/homelab/dns.nix
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.homelab.dns;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.homelab.dns = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Include this host in DNS zone generation";
|
||||||
|
};
|
||||||
|
|
||||||
|
cnames = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = "CNAME records pointing to this host";
|
||||||
|
example = [ "web" "api" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
52
services/ns/external-hosts.nix
Normal file
52
services/ns/external-hosts.nix
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# DNS records for hosts not managed by this flake
|
||||||
|
# These are manually maintained and combined with auto-generated records
|
||||||
|
{
|
||||||
|
aRecords = {
|
||||||
|
# 8_k8s
|
||||||
|
"kube-blue1" = "10.69.8.150";
|
||||||
|
"kube-blue2" = "10.69.8.151";
|
||||||
|
"kube-blue3" = "10.69.8.152";
|
||||||
|
"kube-blue4" = "10.69.8.153";
|
||||||
|
"kube-blue5" = "10.69.8.154";
|
||||||
|
"kube-blue6" = "10.69.8.155";
|
||||||
|
"kube-blue7" = "10.69.8.156";
|
||||||
|
"kube-blue8" = "10.69.8.157";
|
||||||
|
"kube-blue9" = "10.69.8.158";
|
||||||
|
"kube-blue10" = "10.69.8.159";
|
||||||
|
|
||||||
|
# 10
|
||||||
|
"gw" = "10.69.10.1";
|
||||||
|
|
||||||
|
# 12_CORE
|
||||||
|
"virt-mini1" = "10.69.12.11";
|
||||||
|
"nas" = "10.69.12.50";
|
||||||
|
"nzbget-jail" = "10.69.12.51";
|
||||||
|
"restic" = "10.69.12.52";
|
||||||
|
"radarr-jail" = "10.69.12.53";
|
||||||
|
"sonarr-jail" = "10.69.12.54";
|
||||||
|
"bazarr" = "10.69.12.55";
|
||||||
|
"mpnzb" = "10.69.12.57";
|
||||||
|
"pve1" = "10.69.12.75";
|
||||||
|
"inc1" = "10.69.12.80";
|
||||||
|
"inc2" = "10.69.12.81";
|
||||||
|
|
||||||
|
# 22_WLAN
|
||||||
|
"unifi-ctrl" = "10.69.22.5";
|
||||||
|
|
||||||
|
# 30
|
||||||
|
"gunter" = "10.69.30.105";
|
||||||
|
|
||||||
|
# 31
|
||||||
|
"media" = "10.69.31.50";
|
||||||
|
|
||||||
|
# 99_MGMT
|
||||||
|
"sw1" = "10.69.99.2";
|
||||||
|
"testing" = "10.69.33.33";
|
||||||
|
};
|
||||||
|
|
||||||
|
cnames = {
|
||||||
|
# k8s services
|
||||||
|
"rook" = "kube-blue4";
|
||||||
|
"git" = "kube-blue5";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,16 @@
|
|||||||
{ ... }:
|
{ self, lib, ... }:
|
||||||
|
let
|
||||||
|
dnsLib = import ../../lib/dns-zone.nix { inherit lib; };
|
||||||
|
externalHosts = import ./external-hosts.nix;
|
||||||
|
|
||||||
|
# Generate zone from flake hosts + external hosts
|
||||||
|
# Use lastModified from git commit as serial number
|
||||||
|
zoneData = dnsLib.generateZone {
|
||||||
|
inherit self externalHosts;
|
||||||
|
serial = self.sourceInfo.lastModified;
|
||||||
|
domain = "home.2rjus.net";
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
sops.secrets.ns_xfer_key = {
|
sops.secrets.ns_xfer_key = {
|
||||||
path = "/etc/nsd/xfer.key";
|
path = "/etc/nsd/xfer.key";
|
||||||
@@ -26,7 +38,7 @@
|
|||||||
"home.2rjus.net" = {
|
"home.2rjus.net" = {
|
||||||
provideXFR = [ "10.69.13.6 xferkey" ];
|
provideXFR = [ "10.69.13.6 xferkey" ];
|
||||||
notify = [ "10.69.13.6@8053 xferkey" ];
|
notify = [ "10.69.13.6@8053 xferkey" ];
|
||||||
data = builtins.readFile ./zones-home-2rjus-net.conf;
|
data = zoneData;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
{ ... }:
|
{ self, lib, ... }:
|
||||||
|
let
|
||||||
|
dnsLib = import ../../lib/dns-zone.nix { inherit lib; };
|
||||||
|
externalHosts = import ./external-hosts.nix;
|
||||||
|
|
||||||
|
# Generate zone from flake hosts + external hosts
|
||||||
|
# Used as initial zone data before first AXFR completes
|
||||||
|
zoneData = dnsLib.generateZone {
|
||||||
|
inherit self externalHosts;
|
||||||
|
serial = self.sourceInfo.lastModified;
|
||||||
|
domain = "home.2rjus.net";
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
sops.secrets.ns_xfer_key = {
|
sops.secrets.ns_xfer_key = {
|
||||||
path = "/etc/nsd/xfer.key";
|
path = "/etc/nsd/xfer.key";
|
||||||
@@ -24,7 +36,7 @@
|
|||||||
"home.2rjus.net" = {
|
"home.2rjus.net" = {
|
||||||
allowNotify = [ "10.69.13.5 xferkey" ];
|
allowNotify = [ "10.69.13.5 xferkey" ];
|
||||||
requestXFR = [ "AXFR 10.69.13.5@8053 xferkey" ];
|
requestXFR = [ "AXFR 10.69.13.5@8053 xferkey" ];
|
||||||
data = builtins.readFile ./zones-home-2rjus-net.conf;
|
data = zoneData;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
$ORIGIN home.2rjus.net.
|
|
||||||
$TTL 1800
|
|
||||||
@ IN SOA ns1.home.2rjus.net. admin.test.2rjus.net. (
|
|
||||||
2066 ; serial number
|
|
||||||
3600 ; refresh
|
|
||||||
900 ; retry
|
|
||||||
1209600 ; expire
|
|
||||||
120 ; ttl
|
|
||||||
)
|
|
||||||
|
|
||||||
IN NS ns1.home.2rjus.net.
|
|
||||||
IN NS ns2.home.2rjus.net.
|
|
||||||
IN NS ns3.home.2rjus.net.
|
|
||||||
|
|
||||||
; 8_k8s
|
|
||||||
kube-blue1 IN A 10.69.8.150
|
|
||||||
kube-blue2 IN A 10.69.8.151
|
|
||||||
kube-blue3 IN A 10.69.8.152
|
|
||||||
|
|
||||||
kube-blue4 IN A 10.69.8.153
|
|
||||||
rook IN CNAME kube-blue4
|
|
||||||
|
|
||||||
kube-blue5 IN A 10.69.8.154
|
|
||||||
git IN CNAME kube-blue5
|
|
||||||
|
|
||||||
kube-blue6 IN A 10.69.8.155
|
|
||||||
kube-blue7 IN A 10.69.8.156
|
|
||||||
kube-blue8 IN A 10.69.8.157
|
|
||||||
kube-blue9 IN A 10.69.8.158
|
|
||||||
kube-blue10 IN A 10.69.8.159
|
|
||||||
|
|
||||||
; 10
|
|
||||||
gw IN A 10.69.10.1
|
|
||||||
|
|
||||||
; 12_CORE
|
|
||||||
virt-mini1 IN A 10.69.12.11
|
|
||||||
nas IN A 10.69.12.50
|
|
||||||
nzbget-jail IN A 10.69.12.51
|
|
||||||
restic IN A 10.69.12.52
|
|
||||||
radarr-jail IN A 10.69.12.53
|
|
||||||
sonarr-jail IN A 10.69.12.54
|
|
||||||
bazarr IN A 10.69.12.55
|
|
||||||
mpnzb IN A 10.69.12.57
|
|
||||||
pve1 IN A 10.69.12.75
|
|
||||||
inc1 IN A 10.69.12.80
|
|
||||||
inc2 IN A 10.69.12.81
|
|
||||||
media1 IN A 10.69.12.82
|
|
||||||
|
|
||||||
; 13_SVC
|
|
||||||
ns1 IN A 10.69.13.5
|
|
||||||
ns2 IN A 10.69.13.6
|
|
||||||
ns3 IN A 10.69.13.7
|
|
||||||
ns4 IN A 10.69.13.8
|
|
||||||
ha1 IN A 10.69.13.9
|
|
||||||
nixos-test1 IN A 10.69.13.10
|
|
||||||
http-proxy IN A 10.69.13.11
|
|
||||||
ca IN A 10.69.13.12
|
|
||||||
monitoring01 IN A 10.69.13.13
|
|
||||||
jelly01 IN A 10.69.13.14
|
|
||||||
nix-cache01 IN A 10.69.13.15
|
|
||||||
nix-cache IN CNAME nix-cache01
|
|
||||||
actions1 IN CNAME nix-cache01
|
|
||||||
pgdb1 IN A 10.69.13.16
|
|
||||||
nats1 IN A 10.69.13.17
|
|
||||||
auth01 IN A 10.69.13.18
|
|
||||||
vault01 IN A 10.69.13.19
|
|
||||||
vault IN CNAME vault01
|
|
||||||
vaulttest01 IN A 10.69.13.150
|
|
||||||
|
|
||||||
; http-proxy cnames
|
|
||||||
nzbget IN CNAME http-proxy
|
|
||||||
radarr IN CNAME http-proxy
|
|
||||||
sonarr IN CNAME http-proxy
|
|
||||||
ha IN CNAME http-proxy
|
|
||||||
z2m IN CNAME http-proxy
|
|
||||||
grafana IN CNAME http-proxy
|
|
||||||
prometheus IN CNAME http-proxy
|
|
||||||
alertmanager IN CNAME http-proxy
|
|
||||||
jelly IN CNAME http-proxy
|
|
||||||
auth IN CNAME http-proxy
|
|
||||||
lldap IN CNAME http-proxy
|
|
||||||
pyroscope IN CNAME http-proxy
|
|
||||||
pushgw IN CNAME http-proxy
|
|
||||||
|
|
||||||
ldap IN CNAME auth01
|
|
||||||
|
|
||||||
|
|
||||||
; 22_WLAN
|
|
||||||
unifi-ctrl IN A 10.69.22.5
|
|
||||||
|
|
||||||
; 30
|
|
||||||
gunter IN A 10.69.30.105
|
|
||||||
|
|
||||||
; 31
|
|
||||||
media IN A 10.69.31.50
|
|
||||||
|
|
||||||
; 99_MGMT
|
|
||||||
sw1 IN A 10.69.99.2
|
|
||||||
testing IN A 10.69.33.33
|
|
||||||
@@ -11,5 +11,7 @@
|
|||||||
./sops.nix
|
./sops.nix
|
||||||
./sshd.nix
|
./sshd.nix
|
||||||
./vault-secrets.nix
|
./vault-secrets.nix
|
||||||
|
|
||||||
|
../modules/homelab
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user