How to create a router from Centos 8 Stream

Today, I need to rebuild and create a new one linux router. It will work with many networks, there will be DHCP and DNS server as well.

So, at first, we will secure our server on service: ssh. I assume, that you already have enabled firewalld and public zone, only for sshd service. And internal network with dhcp or anything else.

SSHD deamon

At first, we configure our ssh service to be secure. Edit ssh configuration file and adjust there variables:

vim /etc/ssh/sshd_config
Port 22
Protocol 2
PermitRootLogin no
PubkeyAuthentication yes
PermitEmptyPasswords no
PasswordAuthentication no
AllowUsers bob

Now, we create on our client machine (in my place Ubuntu desktop PC) a new rsa-key pair for ssh authentication. You can set some passphrase, but it is you opinion. In quotas, there is a comment for there key pair.

ssh-keygen -t rsa -b 4096 -C "bob@example.com"
means:
-b bits. Number of bits in the key to create
-t type. Specify type of key to create
-C comment

When you had these key pairs generated (under ~/.ssh/) like: id_rsa and id_rsa_pub, you can adjust your server, for accepting these key pair. Now, you must authorize to your server by password (last time).

ssh-copy-id bob@IP-OF-SERVER

If everything is OK, now you can restart your sshd service on server, to load new configurations:

systemctl restart sshd.service

And we can test, that other users is NOT allowed, and there is a need for key authentication:

ssh Alice@192.168.1.2
  Alice@192.168.1.2: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

But Bob is allowed:

ssh bob@192.168.1.2
   Last login: Tue Aug 31 14:47:13 2021 from...

Setting up public interface

We use nmcli (command-line tool for controlling NetworkManager) for watching out of our interfaces:

nmcli device status
DEVICE  TYPE      STATE        CONNECTION 
ens1   ethernet  connected    ens1       
ens2    ethernet  unmanaged    --         
ens3    ethernet  unmanaged    --         
ens4   ethernet  unmanaged    --         
ens5   ethernet  unmanaged    --         
lo      loopback  unmanaged    -- 

Unmanaged interfaces are edited here:

vim /etc/NetworkManager/conf.d/99-unmanaged-devices.conf
[keyfile]
unmanaged-devices=interface-name:ens2;interface-name:ens3;interface-name:ens4;interface-name:ens5

And our ens1 is in config below. UUID we can generate with command:

uuidgen ens1
cat /etc/sysconfig/network-scripts/ifcfg-ens1 
#ens1 - Internal Subnet
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=none
DEFROUTE=no
IPV4_FAILURE_FATAL=no
IPV6_FAILURE_FATAL=no
IPV6INIT=no
NAME=ens1
UUID=e765c8d4-f05e-43b2-96dc-71eef59a908a
DEVICE=ens1
ONBOOT=yes
IPADDR=192.168.1.2
PREFIX=24
GATEWAY=192.168.1.1
DNS1=192.168.1.1

Now, we can configure our public interface. In our example, it will be interface ens2. So, we can copy our first network-script for interface ens2 and edit it. First, generate uuid for this interface. Then adjust your public IP numbers.

uuidgen ens2
cp /etc/sysconfig/network-scripts/ifcfg-ens1 /etc/sysconfig/network-scripts/ifcfg-ens2
vim /etc/sysconfig/network-scripts/ifcfg-ens2

----reboot----

nmcli device status
DEVICE  TYPE      STATE        CONNECTION 
ens1   ethernet  connected    ens1       
ens2    ethernet  connected    ens2  

Now, after some time, we can see many ssh attacks to our system. We can make more things. One is, change the port for ssh daemon. Or I use fail2ban services, for ban IP, that try to hack our server. Like:

Aug 31 13:50:45 r3 sshd[3551]: Invalid user support from 116.98.160.162 port 17306
Aug 31 13:50:48 r3 sshd[3553]: Invalid user geral from 116.98.160.162 port 17006
Aug 31 13:50:49 r3 sshd[3555]: Invalid user omega from 116.110.97.171 port 57344
Aug 31 13:50:51 r3 sshd[3557]: Invalid user matt from 116.98.160.162 port 49776
Aug 31 13:50:57 r3 sshd[3559]: Invalid user rebecca from 116.110.97.171 port 38964
Aug 31 13:51:02 r3 sshd[3561]: Invalid user admin from 171.240.203.115 port 32926
Aug 31 13:51:24 r3 sshd[3564]: Invalid user support from 116.110.97.171 port 49868
Aug 31 13:55:38 r3 sshd[3603]: Invalid user vasilias from 161.97.112.53 port 54654
Aug 31 13:55:42 r3 sshd[3605]: Invalid user admin from 161.97.112.53 port 54656
Aug 31 13:59:53 r3 sshd[3734]: Invalid user user from 2.56.59.30 port 45564

Or we can count it (after two hours). We had 644 ssh login attempts to our server:

cat /var/log/secure | grep Invalid | wc -l
644

So, find I will install fail2ban services, for ban these IPs…

dnf install -y fail2ban-firewalld.noarch

Now, we must create a new jail.local, where we define actions for those IPs and services. So, create this file and add some variables. If you wish, adjust by your needs:

vim /etc/fail2ban/jail.local

[DEFAULT]
# Ban IP/hosts for 24 hour ( 24h*3600s = 86400s):
bantime = 86400
 
# An ip address/host is banned if it has generated "maxretry" during the last "findtime" seconds.
findtime = 900
maxretry = 2

# Enable sshd protection
[sshd]
enabled = true

Now, we can start/restart fail2ban services and view its status. And next, use fail2ban client to view detailed status:

systemctl restart fail2ban
systemctl status fail2ban

fail2ban-client status
fail2ban-client status sshd

Now, we must see some IPs, that are already banned by our fail2ban. If you wish to manually ban/unban IP, lets do this:

fail2ban-client unban 192.168.1.1
fail2ban-client set sshd banip 18.188.124.148

Or you can set your trusted IPs to whitelist:

vim /etc/fail2ban/jail.conf

[DEFAULT]
ignoreip = 127.0.0.1/8 192.168.1.1

Time server – chronyd

Now, we install chronyd service, for obtain and distributing time from world to our subnets. So, install chronyd, and set localisation to you system

dnf install chrony -y
systemctl enable chronyd
timedatectl set-timezone Europe/Bratislava
timedatectl

Now, edit some variables in configurations file for chronyd. Add some servers from pool, and edit local subnets, where we delived time:

vim /etc/chrony.conf

pool 2.centos.pool.ntp.org iburst
pool 1.centos.pool.ntp.org iburst
pool 3.centos.pool.ntp.org iburst
allow 192.168.1.0/24

Now start/restart our service, and check, if it is working:

systemctl restart chronyd
systemctl status chronyd.service
chronyc sources

IPV4 forwarding

As the first things, we must check, if our system has enabled IP forwarding in kernel. Check it out:

sysctl -a | grep ip_forward

net.ipv4.ip_forward = 1

If not, enable it:

sysctl -w net.ipv4.ip_forward=1

vim /etc/sysctl.d/99-sysctl.conf
#insert:
net.ipv4.ip_forward=1

Setting up nftables

In out environment, we use nftables. It is default backend firewall module in Centos 8/ RHEL 8. It is already managed by firewalld, but we gonna use native nft command. The nftables is able to collapse firewall management for IPv4, IPv6 and bridging into the single command line utility: nft.

So, at the beginning (or before run our later nft script), we disable firewalld utility:

systemctl disable --now firewalld
systemctl mask firewalld
reboot

Creating Tables and Chains

Rebooting the system after disabling firewalld will ensure that we have no remnants of the tables, chains and rules added by firewalld. We start up with nothing in-place. Unlike iptables, nftables does not have any tables or chains created by default. Managing nftables natively requires us to create these objects:

protocol family: At the top level of the configuration tree we have protocol families: ip, ip6, arp, bridge, netdev and inet. Using inet we can cater for both IPv4 and IPv6 rules in the one table.

table: Below the protocol family we define tables to group chains together. The default table was the filter table in iptables but we also had other tables such as the nat table.

chain: A chain presents a set of rules that are read from the top down. We stop reading once we match a rule. When creating a chain in nftables we can specify the type of chain – we use the filter type, the hook – we will be working with input hook and a priority. The lower the number the higher the priority and the highest priority chain takes precedence. We will also specify the default policy which defines the action if no rule is met.

To list existing tables, we shouldn’t have any, we use the following:

nft list tables

Now, we create s script file, in which will be all our rules. So, you have to do some learning about nft. Here you are the basic rules:

cat /root/nftables.sh

#!/bin/bash
# the executable for nftables
nft="/usr/sbin/nft"

# wan and lan ports. Home routers typically have two ports minimum.
wan=ens2
lan=ens1
wan6=ens3

# flush/reset rules
${nft} flush ruleset

#create tables called "filter" for ipv4 and ipv6
${nft} add table ip filter
${nft} add table ip6 filter

# one more table called 'nat' for our NAT/masquerading
${nft} add table nat

#create chains
${nft} add chain filter input { type filter hook input priority 0 \; }
${nft} add chain filter output {type filter hook output priority 0 \; }
${nft} add chain filter forward {type filter hook forward priority 0 \; }
${nft} add chain filter postrouting {type filter hook postrouting priority 0 \; }
${nft} add chain nat postrouting {type nat hook postrouting priority 100 \; }

# and for ipv6
${nft} add chain ip6 filter input { type filter hook input priority 0 \; }
${nft} add chain ip6 filter output {type filter hook output priority 0 \; }
${nft} add chain ip6 filter forward {type filter hook forward priority 0 \; }
${nft} add chain ip6 filter postrouting {type filter hook postrouting priority 0 \; }
${nft} add chain ip6 filter nat {type nat hook postrouting priority 100 \; }

#FORWARDING RULESET

#forward traffic from WAN to LAN if related to established context
${nft} add rule filter forward iif $wan oif $lan ct state { established, related } accept

#forward from LAN to WAN always
${nft} add rule filter forward iif $lan oif $wan accept

#drop everything else from WAN to LAN
${nft} add rule filter forward iif $wan oif $lan counter drop

#ipv6 just in case we have this in future.
${nft} add rule ip6 filter forward iif $wan6 oif $lan ct state { established,related } accept
${nft} add rule ip6 filter forward iif $wan6 oif $lan icmpv6 type echo-request accept

#forward ipv6 from LAN to WAN.
${nft} add rule ip6 filter forward iif $lan oif $wan6 counter accept

#drop any other ipv6 from WAN to LAN
${nft} add rule filter forward iif $wan6 oif $lan counter drop

#INPUT CHAIN RULESET
#============================================================================
${nft} add rule filter input ct state { established, related } accept

# always accept loopback
${nft} add rule filter input iif lo accept
# comment next rule to disallow ssh in
${nft} add rule filter input tcp dport ssh counter log accept

#accept DNS, SSH, from LAN
${nft} add rule filter input iif $lan tcp dport { 53, 22 } counter log accept
#accept DNS on LAN
${nft} add rule filter input iif $lan udp dport { 53 } accept

#accept ICMP on the LAN,WAN
${nft} add rule filter input iif $lan ip protocol icmp accept
${nft} add rule filter input iif $wan ip protocol icmp accept

${nft} add rule filter input counter drop

################# ipv6 ###################
${nft} add rule ip6 filter input ct state { established, related } accept
${nft} add rule ip6 filter input iif lo accept
#uncomment next rule to allow ssh in over ipv6
#${nft} add rule ip6 filter input tcp dport ssh counter log accept

${nft} add rule ip6 filter input icmpv6 type { nd-neighbor-solicit, echo-request, nd-router-advert, nd-neighbor-advert } accept

${nft} add rule ip6 filter input counter drop


#OUTPUT CHAIN RULESET
#=======================================================
# allow output from us for new, or existing connections.
${nft} add rule filter output ct state { established, related, new } accept

# Always allow loopback traffic
${nft} add rule filter output iif lo accept

${nft} add rule ip6 filter output ct state { established, related, new } accept
${nft} add rule ip6 filter output oif lo accept


#SET MASQUERADING DIRECTIVE
${nft} add rule nat postrouting masquerade

So, this script can be run by:

sh /root/nftables.sh

And now, we can view our filter and rules:

nft list tables
nft list table filter

This is only for this session, and after reboot, there will be empty list. To ensure, that filter persist reboot, run following:

nft list ruleset > /etc/sysconfig/nftables.conf
systemctl enable nftables.service
reboot
nft list table inet filter

Install and create own DNS resolver

Now we set up a local DNS resolver on CentOS 8/RHEL 8, with the widely-used BIND9 DNS software.

Be aware that a DNS server can also called a name server. Examples of DNS resolver are 8.8.8.8 (Google public DNS server) and 1.1.1.1 (Cloudflare public DNS server).

Normally, your computer or router uses your ISP’s DNS resolver to query domain names in order to get an IP address. Running your own local DNS resolver can speed up DNS lookups, because

  1. The local DNS resolver only listens to your DNS requests and does not answer other people’s DNS requests, so you have a much higher chance of getting DNS responese directly from the cache on the resolver.
  2. The network latency between your computer and DNS resolver is eliminated (almost zero), so DNS queries can be sent to root DNS servers more quickly.

If you own a website and want your own DNS server to handle name resolution for your domain name instead of using your domain registrar’s DNS server, then you will need to set up an authoritative DNS server, which is different than a DNS resolver. BIND can act as an authoritative DNS server and a DNS resolver at the same time

BIND (Berkeley Internet Name Domain) is an open-source DNS server software widely used on Unix/Linux due to it’s stability and high quality.

So, install it first, check version and start it. The BIND daemon is called named.

dnf update
dnf install bind bind-utils

named -v
    BIND 9.11.26-RedHat-9.11.26-6.el8 (Extended Support Version) <id:3ff8620>

systemctl start named
systemctl enable named

Usually, DNS queries are sent to the UDP port 53. The TCP port 53 is for responses size larger than 512 bytes. You must adjust your firewall to allow tcp and udp packets on port 53.

We can check the status of the BIND name server.:

rndc status

Configurations for a Local DNS Resolver

Out of the box, the BIND9 server on CentOS/RHEL provides recursive service for localhost only. Outside queries will be denied. Edit the BIND main configuration file:

vim /etc/named.conf

In the options clause, you can find the following two lines:

listen-on port 53 { 127.0.0.1; };
listen-on-v6 port 53 { ::1; };

This makes named listen on localhost only. If you want to allow clients in the same network to query domain names, then comment out these two lines. (add double slashes at the beginning of each line)

// listen-on port 53 { 127.0.0.1; };
// listen-on-v6 port 53 { ::1; };

Find and adjust these variables:

allow-query { localhost; 192.168.1.0/24; };
recursion yes;
 // hide version number from clients for security reasons.
 version "not currently available";
 // enable the query log
 querylog yes;

Now, check our config (silent output if is OK), and restart named:

named-checkconf
systemctl restart named

Now, you have enabled DNS resolver.

Create own DNS zone, dnssec signed

If you want your own DNS server for your domain, accessible from world and you don’t want you domain provider, to be a DNS server for this domain, let read next.

First, create a new database file for this domain:

vim /var/named/db.example.com

;
; BIND data file for example.com
;
$TTL 604800
@       IN      SOA     example.com.     root.example.com.        (
        2021090608      ; Serial
        3600            ; Refresh
        86400           ; Retry
        2419200         ; Expire
        3600    )       ; Default TTL

        IN      NS      ns1.example.com.

@	IN	A	212.5.215.172
ns1     IN      A       212.5.215.172
www	IN	CNAME	ns1

We have three dns names, that direct to our domain (www, ns1 and the domain itself: example.com). Now, we must edit the main configuration file for named, and add point to this zone/domain/file. And add som variables for dnssec. Now we comment out previously commented two lines and edit some variables:

vim /etc/named.conf

        listen-on port 53 { any; };
        listen-on-v6 port 53 { any; };
        allow-query { any; };
        dnssec-enable yes;
        dnssec-validation yes;
        recursion no;


zone    "example.com" IN {
        type master;
        file "db.example.com";
        allow-transfer { none; };
};

Now, check the config and restart named:

named-checkconf
named-checkzone example.com /var/named/db.example.com
systemctl restart named

Now, we can reach our domain from world, if domain registrator set NameServer (ns1.example.com) to point to our public IP and let us for managing our dns zone.

Now, we can continue for dnssec with some theory:

DNS by itself is not secure. DNS was designed in the 1980s when the Internet was much smaller, and security was not a primary consideration in its design. As a result, when a recursive resolver sends a query to an authoritative name server, the resolver has no way to verify the authenticity of the response. The resolver can only check that a response appears to come from the same IP address where the resolver sent the original query. But relying on the source IP address of a response is not a strong authentication mechanism, since the source IP address of a DNS response packet can be easily forged, or spoofed.

DNSSEC strengthens authentication in DNS using digital signatures based on public key cryptography. With DNSSEC, it’s not DNS queries and responses themselves that are cryptographically signed, but rather DNS data itself is signed by the owner of the data…. More about this: https://www.icann.org/resources/pages/dnssec-what-is-it-why-important-2019-03-05-en

So, lets create a key-pairs:

1 – Create a Zone Signing Key (ZSK)
2 – Create a Key Signing Key (KSK)

cd /var/named
dnssec-keygen -a RSASHA256 -b 2048 -r /dev/urandom example.com
dnssec-keygen -a RSASHA256 -b 4096 -r /dev/urandom -f KSK example.com

eg: Generating key pair......................++ .............................................................................................................................................................................................................++
Kexample.com.+007+18522

The directory will now have 4 keys – private/public pairs of ZSK and KSK. We have to add the public keys which contain the DNSKEY record to the zone file. For example:

cat Kexample.com.+008+18522.key >> /var/named/db.example.com
cat Kexample.com.+008+56121.key >> /var/named/db.example.com

Now, edit owner permisions of these keys to named:

chown named Kexample.com*

And now, check our zone and if is everything ok, sign the zone:

named-checkzone example.com /var/named/db.example.com

zone example.com/IN: loaded serial 2021090901
OK

dnssec-signzone -A -3 $(head -c 1000 /dev/urandom | sha1sum | cut -b 1-16) -N INCREMENT -o example.com -t db.example.com

Verifying the zone using the following algorithms: RSASHA256.
Zone fully signed:
Algorithm: RSASHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                      ZSKs: 1 active, 0 stand-by, 0 revoked
db.example.com.signed
Signatures generated:                       13
Signatures retained:                         0
Signatures dropped:                          0
Signatures successfully verified:            0
Signatures unsuccessfully verified:          0
Signing time in seconds:                 0.020
Signatures per second:                 649.707
Runtime in seconds:                      0.025

Above command created a signed zone file for example.com zone: db.example.com.signed.

We are now required to edit zone configuration to use new file instead of db.example.com old file, and add som variables:

vim /etc/named.conf

zone    "example.com" IN {
        type master;
        file "db.example.com.signed";
        allow-transfer { none; };
   # DNSSEC keys Location
   key-directory "/var/named/"; 

   # Publish and Activate DNSSEC keys
   auto-dnssec maintain;


   # Use Inline Signing
   inline-signing yes;

};

Now, check named config, and our zone:

named-checkconf
named-checkzone example.com /var/named/db.example.com.signed

zone example.com/IN: loaded serial 2021090902 (DNSSEC signed)
OK

systemctl restart named

And now, we can try our zone, if dnssec is working:

dig DNSKEY example.com. +multiline

; <<>> DiG 9.11.26-RedHat-9.11.26-6.el8 <<>> DNSKEY example.com. +multiline
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19926
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;example.com.		IN DNSKEY

;; ANSWER SECTION:
example.com.		604795 IN DNSKEY 256 3 8 (
				AwEAAbfz4OM2hWwBKTlQuvP+q5SxJWkQo0SYbHLjJKiw
				J0qwgsTsXQM8uYa8tHcFdZFSNu7Qrs14CHEGimsufpGO
				d1TgRUeJxDI9Yrl27hg+rd+58HUiwkxQpTOFS4FXx2mw
				/TsTREFtiC16H2ZA/Bgw4N/C4HO2JfBIyOt6YdA4labS
				KBGtBXcR5tbckXh706JwAaVFliDDnFNkPh0L5UUDpkUG
				eKLVis4n7WqQdtv0/WRbURm0HiyMwAdovvr1q1YUmRni
				xAN/5lBAWOID6YETSx+QROEoxkveHJpctE3knRTC3ZnR
				KqF6z25nrWv9oJeghk6niq6Eyt5+dibMLJaJ8Cc=
				) ; ZSK; alg = RSASHA256 ; key id = 56241
example.com.		604795 IN DNSKEY 257 3 8 (
				AwEAAduWjrX7Rv6A3b7VA0t0Q2dQLawRP9F4A86eIScD
				FpDpGFjp8T6U2nFcNrqlQrgXKJ5DUXIxkWZd2mbtjTWn
				TDp6cjnuudYispnV3EyiXhhpGUyEBlOGUvZ8f55xd3EK
				f+p1inceqzYL0VV2qaAMmPBR8gUhtDDj9hlIhJfEC0b9
				JeIVHO01U0IIFsHCDNnykLwpibeK1e/TWwQ3ipoaHshJ
				dJS3ZdfFS0zHBn736v5wneJVusR5AaBcFbeVWdW3bZR9
				EPwh97nVEdvZk2UVfQtDy3KNpwzbvaB19EbBdaeiDEr8
				8+Tho3vRuo1uphhFtTbWWMHnyY5WKDcmBEtAVH1WhsEW
				p4cyMa4ASVz8Zr10b88g7EN8i4lm6X7hNFVVjZ75N+MI
				YNbuxHF1C7FDh3ACKVMk8qveYQx37iS8G5RID8COzpej
				N9L+o/3xlAd6k8hnKqvU+TE1xcOM2Q936Rpudzs6vKDz
				Qvy5h94J9b/5ZPkb4CPOto9jWzT/t/lv96cvtG8qKTDH
				Spg9WtTf42DSdFHflTof5Fqlzjy2Fq0UQXlJNGjOHoii
				TBcQATlSC1yxQALj3+1fvTSe9yZ+PGwxnGUKijTvfcpP
				OetqN8T9261Kv5u/kOKjDM9DxLYYdfkqV6dKAooWaIUS
				wPFkk+zbN7YnNoasHRGSF+/hC+lV
				) ; KSK; alg = RSASHA256 ; key id = 18544

We have configured DNSSEC on our master DNS server.

DHCP server on lan

Now, we install dhcp server on our centos server. I have interface for this server: ens22, and our gateway will be 192.168.15.9 and clients will have IPs from range: 192.168.15.20-192.168.15.100. We must have a static IP from this subnet on this interface.

dnf makecache
dnf install dhcp-server

Now, create own configuration file for dhcp server, or copy example and adjust:

cp /usr/share/doc/dhcp-server/dhcpd.conf.example /etc/dhcp/dhcpd.conf
#or:
vim /etc/dhcp/dhcpd.conf

And edit this variables as you fit:

option domain-name "example.com";
option domain-name-servers ns1.example.com;

default-lease-time 600;
max-lease-time 7200;

ddns-update-style none;
authoritative;
log-facility local7;

subnet 192.168.15.0 netmask 255.255.255.0 {
}
subnet 192.168.15.0 netmask 255.255.255.0 {
  range 192.168.15.20 192.168.15.100;
  option routers 192.168.15.9;
  option domain-name-servers 192.168.15.9;
}

Now, we can start dhcp server and view our logs. I have 8 interfaces, but only on this is dhcp enabled:

systemctl start dhcpd.service
systemctl enable dhcpd.service
tail /var/log/messages -f

No subnet declaration for ens18 (no IPv4 addresses).
 ** Ignoring requests on ens18.  If this is not what
you want, please write a subnet declaration
in your dhcpd.conf file for the network segment
to which interface ens18 is attached. **

Now, enable UDP port 67 to access dhcp server. Look below for this port. So edit your nftables.sh script and apply it and view:

netstat -tulpen | grep dhcp
udp        0      0 0.0.0.0:67

iif "ens22" udp dport { 67 } accept

And start some PC on this network and we will see at logs:

tail -n 10 /var/log/messages
dhcpd[]: DHCPREQUEST for 192.168.15.20 from ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPACK on 192.168.15.20 to ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPREQUEST for 192.168.15.20 from ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPACK on 192.168.15.20 to ac:5a:32:98:0c:xx (pc1) via ens22
Total Page Visits: 98 - Today Page Visits: 1