~~~~~~~~~~
sudo apt-get install mtr sudo tcpdump
~~~~~~~~~~
Install Quagga (FRR is similar as it is a drived distribution)
sudo apt-get install quagga
Create configuration files
sudo touch /etc/quagga/zebra.conf /etc/quagga/ospfd.conf /var/log/quagga/zebra.log /var/log/quagga/ospfd.log
sudo chown quagga:quagga /etc/quagga/{zebra,ospfd}.conf
sudo chown quagga:quagga /var/log/quagga/{zebra,ospfd}.log
sudo chmod o-r /etc/quagga/{zebra,ospfd}.conf
sudo chmod o-r /var/log/quagga/{zebra,ospfd}.log
Edit /etc/quagga/daemons and enable zebra and ospfd, optionally enable and configure ospf6d if your network has IPv6 connectivity
Populate the config files
sudo cat > /etc/quagga/daemons <<DONE
zebra=yes
bgpd=no
ospfd=yes
ospf6d=yes
ripd=no
ripngd=no
isisd=no
babeld=no
DONE
sudo cat > /etc/quagga/zebra.conf <<DONE
password <CONNECT PASSWORD>
enable password <ENABLE PASSWORD>
log file /var/log/quagga/zebra.log
DONE
sudo cat > /etc/quagga/ospfd.conf <<DONE
password <CONNECT PASSWORD>
enable password <ENABLE PASSWORD>
log file /var/log/quagga/ospfd.conf
interface <INTERFACE>
ip ospf authentication message-digest
ip ospf message-digest-key 1 md5 <OSPF PASSWORD>
ip ospf priority 10
router ospf
ospf router-id <PRIMARY SERVER IP>
redistribute connected
distribute-list AMPR out connected
network <LAN NETWORK ADDRESS/BITMASK> area 0.0.0.0
network <ANYCAST NETWORK/BITMASK> area 0.0.0.0
area 0 authentication message-digest
access-list AMPR permit 44.0.0.0/9
access-list AMPR permit 44.128.0.0/10
DONE
sudo cat > /etc/quagga/ospf6d.conf <<DONE
password <CONNECT PASSWORD>
enable password <ENABLE PASSWORD>
log file /var/log/quagga/ospf6d.log
interface eth0
ipv6 ospf6 priority 10
interface lo
router ospf6
router-id <PRIMARY IPv4 SERVER IP>
redistribute connected
interface eth0 area 0.0.0.0
interface lo area 0.0.0.0
area 0.0.0.0 range <IPv6 ANYCAST SUBNET 1>
area 0.0.0.0 range <IPv6 ANYCAST SUBNET 2>
DONE
(Note that for Ubuntu you’ll instead need to manually edit the files rather than using cat > syntax)
(Note that in this case, the
(Your anycast network’s bitmask is almost certainly 23)
Start Quagga
sudo service quagga start
Verify OSPF is working
ip r | wc -l # Should show a large number of routes, > 300 at present.
Remove previous static default route
sudo ip r | grep default # If it contains the word "zebra", do not remove it
sudo ip r del default # Be sure you have OOB control first since this can disconnect you
sudo ip r | grep default # Should now have a default route from zebra
sudo vim /etc/network/interfaces # Remove any gateway statement if this was a static config
OPTIONAL: Verify everything will work from a cold boot
ifdown eth0 ; ifup eth0 # Be sure you have OOB control first since this can disconnect you
OPTIONAL: Verify you can control zebra + ospfd
telnet localhost 2601 # For zebra
telnet localhost 2604 # For ospfd
telnet 1 2606 # For ospf6d
FRR is a fork of the previous Quagga router and is used on more modern distributions.
Install FRR
sudo apt-get install frr
Create configuration files
sudo touch /var/log/frr/frr.log
sudo chown frr:frr /var/log/frr/frr.log
sudo chmod o-r /var/log/frr/frr.log
Populate the config files (ospf6 not tested). We enable ospfd and optionally ospf6d.
sudo sed -i -e 's/ospfd=no/ospfd=yes/' /etc/frr/daemons
sudo sed -i -e 's/ospf6d=no/ospf6d=yes/' /etc/frr/daemons
sudo cat >> /etc/frr/frr.conf <<DONE
password <CONNECT PASSWORD>
enable password <ENABLE PASSWORD>
log file /var/log/frr/frr.log
interface <INTERFACE>
ip ospf authentication message-digest
ip ospf message-digest-key 1 md5 <OSPF PASSWORD>
ip ospf priority 10
ipv6 ospf6 priority 10
interface lo
router ospf
ospf router-id <PRIMARY SERVER IP>
redistribute connected
distribute-list AMPR out connected
network <LAN NETWORK ADDRESS/BITMASK> area 0.0.0.0
network <ANYCAST NETWORK/BITMASK> area 0.0.0.0
area 0 authentication message-digest
router ospf6
router-id <PRIMARY IPv4 SERVER IP>
redistribute connected
interface eth0 area 0.0.0.0
interface lo area 0.0.0.0
area 0.0.0.0 range <IPv6 ANYCAST SUBNET 1>
area 0.0.0.0 range <IPv6 ANYCAST SUBNET 2>
access-list AMPR permit 44.0.0.0/9
access-list AMPR permit 44.128.0.0/10
DONE
Start FRR
sudo systemctl restart frr
Verify OSPF is working
ip r | wc -l # Should show a large number of routes, > 300 at present.
Remove previous static default route
sudo ip r | grep default # Determine if its static or dynamic from zebra. If dynamic, do not remove.
sudo ip r del default # Be sure you have OOB control first since this can disconnect you
sudo ip r | grep default # Should now have a default route from zebra
sudo vim /etc/network/interfaces # Convert to static config with no gateway statement.
OPTIONAL: Verify everything will work from a cold boot
ifdown eth0 ; ifup eth0 # Be sure you have OOB control first since this can disconnect you
OPTIONAL: Verify you can control zebra + ospfd
telnet localhost 2601 # For zebra
telnet localhost 2604 # For ospfd
telnet 1 2606 # For ospf6d
Install unbound
sudo apt-get install unbound
Stop and disable unbound
sudo service unbound stop
sudo update-rc.d unbound disable
Configure unbound
sudo cat > /etc/unbound/unbound.conf.d/hamwan.conf <<DONE
server:
# The following line will configure unbound to perform cryptographic
# DNSSEC validation using the root trust anchor.
auto-trust-anchor-file: "/var/lib/unbound/root.key"
interface: <ANYCAST IP 1> # e.g. 44.24.244.1
interface: <ANYCAST IP 2>
interface: <ANYCAST IP 3>
interface: <ANYCAST IP 4>
do-ip6: no
# interface: <IPv6 ANYCAST IP 1> # e.g. 2604:5000:20:1::1
# interface: <IPv6 ANYCAST IP 2>
access-control: 44.0.0.0/9 allow
access-control: 44.128.0.0/10 allow
# access-control: <IPv6 ALLOCATION> allow # e.g. 2604:5000:20::/48 allow
outgoing-interface: <PRIMARY SERVER IP> # e.g. 44.25.16.10
rrset-roundrobin: yes
logfile: /var/log/unbound.log
log-time-ascii: yes
val-permissive-mode: yes
local-zone: "10.in-addr.arpa." nodefault
stub-zone:
name: "hamwan.net"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "44.10.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "240.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "241.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "242.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "243.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "244.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "245.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "246.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "247.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "248.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "249.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "250.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "251.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "252.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "253.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "254.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "255.24.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
stub-zone:
name: "25.44.in-addr.arpa"
stub-addr: 44.24.244.2
stub-addr: 44.24.245.2
stub-addr: 44.25.0.2
stub-addr: 44.25.1.2
stub-prime: no
stub-first: no
DONE
Update apparmor to allow unbound to log
cat >> /etc/apparmor.d/local/usr.sbin/unbound <<DONE
# Site-specific additions and overrides for usr.sbin.unbound.
# For more details, please see /etc/apparmor.d/local/README.
/var/log/unbound.log rw,
DONE
apparmor_parser -r /etc/apparmor.d/usr.sbin.unbound
Configure recursive DNS IPv4 anycast interface
If using Quagga on older systems:
sudo cat >> /etc/network/interfaces <<DONE
auto any-dns-rr
iface any-dns-rr inet manual
pre-up ip tuntap add dev any-dns-rr mode tap
post-up ip a add <ANYCAST IP 1>/32 dev any-dns-rr
post-up ip a add <ANYCAST IP 2>/32 dev any-dns-rr
post-up ip l set dev any-dns-rr up
post-up service unbound start
post-down service unbound stop
post-down ip tuntap del dev any-dns-rr mode tap
DONE
If using FRR on newer systems, check first to see if there is already a loopback (lo) interface specification and if so, just add the anycast addresses (this may work for Quagga system too):
sudo cat >> /etc/network/interfaces
auto lo
iface lo inet loopback
post-up ip a add <ANYCAST IP 1>/32 dev lo
post-up ip a add <ANYCAST IP 2>/32 dev lo
<CTRL-D>
OPTIONALLY: Configure recursive DNS IPv6 anycast interface
auto lo
iface lo inet loopback
post-up ip -6 a add <IPv6 ANYCAST IP 1>/128 dev lo
post-up ip -6 a add <IPv6 ANYCAST IP 2>/128 dev lo
Start the recursive DNS resolver service
sudo ifup lo
sudo ifup any-dns-rr # if using quagga
systemctl enable unbound
systemctl start unbound
Verify functionality
ip a # Should see the any-dns-rr interface with the two anycast IPs as well as the IPv6 IPs on the lo interface
dig @<ANYCAST IP 1> google.com. A # Should return google's IPs
dig @<IPv6 ANYCAST IP 1> google.com. A # Should return google's IPs
Verify the service is being advertised to OSPF
ssh <NEAREST OSPF ROUTER>
/ip route check <ANYCAST IP 1> # Should display nearest server's primary IP as nexthop
/ipv6 route check <IPv6 ANYCAST IP 1> # Should display Link Local address of nearest server's ethernet interface as nexthop
Authoritative DNS is used to place names for the hostnames and the necessary PTR records for reverse-dns. It is comprised of a PowerDNS install utilizing PostgreSQL. You will amostly certainly want to install the HamWAN Management Portal alongside this.
Install necessary software
sudo apt-get install postgresql postgresql-contrib postgresql-client pdns-server pdns-backend-pgsql
Setup the anycast IPs
Similar to above add anycast addresses for authoritative DNS service. For older systems:
sudo cat >> /etc/network/interfaces <<DONE
auto any-adns
iface any-adns inet manual
pre-up ip tuntap add dev any-adns mode tap
pre-up ip l set dev any-adns mtu 1418
post-up ip a add <ANYCAST IP 1>/32 dev any-adns
post-up ip a add <ANYCAST IP 2>/32 dev any-adns
post-up ip l set dev any-adns up
post-up service pdns restart
post-down ip tuntap del dev any-adns mode tap
DONE
and for newer systems, just add the address to the loopback interface:
iface lo inet loopback
...
post-up ip a add <ANYCAST IP 1>/32 dev lo
post-up ip a add <ANYCAST IP 2>/32 dev lo
Make sure postgres can handle our connection properly
Where VERSION is the installed postgresql version (you will need to check first)
sudo vi /etc/postgresql/VERSION/main/pg_hba.conf
# in this file, make sure that the following line is present (most likely you'll change auth from "peer" to "md5")
local all all md5
Update the pdns config to point to the postgres db; make sure the following are set in /etc/powerdns/pdns.d/pdns.local.gpgsql.conf. Make sure there aren’t any include-dir statements. Remove bind.conf. We don’t need it.
rm /etc/powerdns/pdns.d/bind.conf
cat > /etc/powerdns/pdns.d/pdns.local.gpgsql.conf <<DONE
launch=gpgsql
gpgsql-host=
gpgsql-port=
gpgsql-user=powerdns
gpgsql-password=<DB PW>
gpgsql-dbname=powerdns
gpgsql-dnssec=yes
local-address=<YOUR ANYCAST AUTHORITATIVE NS IPS SEPARATED BY COMMA>
DONE
cat > /etc/powerdns/pdns.d/allow-44-axfr.conf <<DONE
allow-axfr-ips=44.0.0.0/8
disable-axfr=no
DONE
Setup the DB if this is going to be a master. If this is going to be a slave replica, go replica setup below.
sudo su postgres ; change to the postgres user
psql ; enter the postgres prompt
CREATE USER powerdns WITH PASSWORD '<DB PW>';
CREATE DATABASE powerdns;
\q
Create the PDNS DB Scema
psql -d powerdns
CREATE TABLE domains (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
master VARCHAR(128) DEFAULT NULL,
last_check INT DEFAULT NULL,
type VARCHAR(6) NOT NULL,
notified_serial INT DEFAULT NULL,
account VARCHAR(40) DEFAULT NULL,
CONSTRAINT c_lowercase_name CHECK (name = LOWER(name))
);
CREATE UNIQUE INDEX name_index ON domains(name);
CREATE TABLE records (
id SERIAL PRIMARY KEY,
domain_id INT DEFAULT NULL,
name VARCHAR(255) DEFAULT NULL,
type VARCHAR(10) DEFAULT NULL,
content VARCHAR(65535) DEFAULT NULL,
ttl INT DEFAULT NULL,
prio INT DEFAULT NULL,
change_date INT DEFAULT NULL,
disabled BOOL DEFAULT 'f',
ordername VARCHAR(255),
auth BOOL DEFAULT 't',
CONSTRAINT domain_exists
FOREIGN KEY(domain_id) REFERENCES domains(id)
ON DELETE CASCADE,
CONSTRAINT c_lowercase_name CHECK (name = LOWER(name))
);
CREATE INDEX rec_name_index ON records(name);
CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);
CREATE INDEX recordorder ON records (domain_id, ordername text_pattern_ops);
CREATE TABLE supermasters (
ip INET NOT NULL,
nameserver VARCHAR(255) NOT NULL,
account VARCHAR(40) DEFAULT NULL,
PRIMARY KEY(ip, nameserver)
);
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
domain_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(10) NOT NULL,
modified_at INT NOT NULL,
account VARCHAR(40) DEFAULT NULL,
comment VARCHAR(65535) NOT NULL,
CONSTRAINT domain_exists
FOREIGN KEY(domain_id) REFERENCES domains(id)
ON DELETE CASCADE,
CONSTRAINT c_lowercase_name CHECK (name = LOWER(name))
);
CREATE INDEX comments_domain_id_idx ON comments (domain_id);
CREATE INDEX comments_name_type_idx ON comments (name, type);
CREATE INDEX comments_order_idx ON comments (domain_id, modified_at);
CREATE TABLE domainmetadata (
id SERIAL PRIMARY KEY,
domain_id INT REFERENCES domains(id) ON DELETE CASCADE,
kind VARCHAR(32),
content TEXT
);
CREATE INDEX domainidmetaindex ON domainmetadata(domain_id);
CREATE TABLE cryptokeys (
id SERIAL PRIMARY KEY,
domain_id INT REFERENCES domains(id) ON DELETE CASCADE,
flags INT NOT NULL,
active BOOL,
content TEXT
);
CREATE INDEX domainidindex ON cryptokeys(domain_id);
CREATE TABLE tsigkeys (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
algorithm VARCHAR(50),
secret VARCHAR(255),
CONSTRAINT c_lowercase_name CHECK (name = LOWER(name))
);
CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);
GRANT SELECT ON supermasters TO powerdns;
GRANT ALL ON tsigkeys TO powerdns;
GRANT ALL ON cryptokeys TO powerdns;
GRANT ALL ON domainmetadata TO powerdns;
GRANT ALL ON comments TO powerdns;
GRANT ALL ON records TO powerdns;
GRANT ALL ON domains TO powerdns;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO powerdns;
\q
Restart the deamons
exit; go back to your normal user
sudo service postgresql restart
# if using any-adns pseudo interface...
sudo ifup any-adns
Set up database replication if this is a slave server, starting with the master. References: - https://www.postgresql.org/docs/15/warm-standby.html#STREAMING-REPLICATION - https://www.postgresql.fastware.com/postgresql-insider-ha
# update pg_hba.conf to add a replication user
# configure replication parameters in postgresql.conf
Configure the slave postgresql server by editing postgresql.conf References: - https://www.postgresql.org/docs/15/warm-standby.html#STREAMING-REPLICATION - https://www.postgresql.fastware.com/postgresql-insider-ha
systemctl postgresql stop
# The standby connects to the primary that is running on host 192.168.1.50
# and port 5432 as the user "foo" whose password is "foopass".
primary_conninfo = 'host=192.168.1.50 port=5432 user=foo password=foopass'
# ensure database location is missing or empty, then fetch a current copy of the database
# run pg_basebackup ... ('-X none' because of copy from old system)
pg_basebackup -D /var/lib/postgresql/9.1/main -X none -v -h SRV1.westin.hamwan.net -U ziply_copy -W
pg_upgradecluster
Test it!
sudo service pdns stop #stop the service for testing
sudo /etc/init.d/pdns monitor # you shouldn't see any errors during the startup;
# go back to normal
sudo service pdns start
Install software
sudo apt-get update #make sure we're up to date
#install the webserver, python, and the python libs we need
sudo apt-get install nginx python3-certbot-nginx python3-pip python3-virtualenv python3-dev git libpq-dev postgresql postfix
#we use this user to run uwsgi server; use a good password!!
sudo adduser portal
# Time to download our custom stuff. /var/www should already exist.
cd /var/www
sudo git clone https://github.com/HamWAN/dns-portal.git
sudo git clone https://github.com/HamWAN/django-ssl-client-auth.git
sudo chown portal:portal -R dns-portal django-ssl-client-auth
sudo -s -u portal
Now, as the portal user…
cd dns-portal
# make a symlink for the ssl auth stuff
ln -s /var/www/django-ssl-client-auth/django_ssl_auth .
# Load addtional python modules in a virtual environment
virtualenv env #setup our virtual environment
source env/bin/activate
# We should now be in the virtual environment
# Install server and dependencies via pip instead of apt-get to get the latest and keep contained
pip3 install -r requirements.txt
pip3 install uwsgi south django-debug-toolbar # currently missing from requirements.txt
exit
Done with this part! On to the database… You can’t cut/paste this block. Work in chunks. You need to be mindful of the changing input contexts.
sudo -i -u postgres
psql
CREATE USER portal WITH PASSWORD '<your-portal-user-pw-here>';
CREATE DATABASE portal;
\q
psql -d portal
GRANT ALL PRIVILEGES ON SCHEMA public TO portal;
CREATE OR REPLACE FUNCTION array_reverse(anyarray) RETURNS anyarray AS $$
SELECT ARRAY(
SELECT $1[i]
FROM generate_subscripts($1,1) AS s(i)
ORDER BY i DESC
);
$$ LANGUAGE 'sql' STRICT IMMUTABLE;
\q
exit
Now we need to configure settings.py to point to our database
sudo -s -u portal
cd dns-portal
source env/bin/activate #get back in our virtual environment
Now we need to edit our config/settings.py file; this is a bit complicated, but use the following as a template:
# Django settings for this portal implementation project.
# TODO(implementer): Reset to False for Production
DEBUG = True
TEMPLATE_DEBUG = DEBUG
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
ROOT_DOMAIN = 'hamwan.net'
DEFAULT_NETWORK = '44.25.0.0/16'
AUTH_USER_MODEL = 'auth.User'
ADMINS = (
('Tom Hayward', 'tom@tomh.us'),
('Doug Kingston', 'dpk@randomnotes.org'),
)
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'portal', # Or path to database file if using sqlite3.
# The following settings are not used with sqlite3:
'USER': 'portal',
'PASSWORD': 'secure-password-here', # Using uid auth, you won't need a password
'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
'PORT': '', # Set to empty string for default.
},
'pdns': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'powerdns', # Or path to database file if using sqlite3.
'USER': 'pdns',
'PASSWORD': 'another-secure-password',
'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
'PORT': '', # Set to empty string for default.
},
}
# Uncomment in prod where pdns is a separate database
DATABASE_ROUTERS = ['config.dbrouter.DnsRouter', ]
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ['portal.hamwan.org', 'encrypted.hamwan.org',
'portal.hamwan.net', 'encrypted.hamwan.net',
'portal.ziply.hamwan.net', 'srv2.ziply.hamwan.net',]
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'America/Los_Angeles'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
# Confirm this setting at your in your Sites table. The number here
# needs to match the index of your site in the Sites table.
SITE_ID = 2
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/var/www/example.com/media/"
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://example.com/media/", "http://media.example.com/"
MEDIA_URL = ''
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/"
STATIC_ROOT = '/var/www/hamwan-portal/static/'
# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'
# Additional locations of static files
STATICFILES_DIRS = (
# Put strings here, like "/home/html/static" or "C:/www/django/static".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
"/var/www/hamwan-portal/css",
"/var/www/hamwan-portal/config/css",
)
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'your-strong-unique-key-here'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
# insert your TEMPLATE_DIRS here
'/var/www/hamwan-portal/templates',
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
# Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
# list if you haven't customized them:
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
# HamWAN portal additions
'django.template.context_processors.request',
'portal.context_processors.encrypted44',
],
},
},
]
MIDDLEWARE = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.RemoteUserMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'config.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'config.wsgi.application'
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'django.contrib.flatpages',
#disabled#configuration,
#disabled#api
'dns',
'portal',
# 'map',
#disabled#rest_framework
'utils',
# Uncomment the next line to enable the request and query debugging tool
# 'debug_toolbar',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
)
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
PRODUCTION_LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
DEBUG_LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
'datefmt' : "%d/%b/%Y %H:%M:%S"
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': '/tmp/mysite.log',
'formatter': 'verbose'
},
},
'loggers': {
'django': {
'handlers':['file'],
'propagate': True,
'level':'DEBUG',
},
'MYAPP': {
'handlers': ['file'],
'level': 'DEBUG',
},
}
}
LOGGING=PRODUCTION_LOGGING
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
Use the django framework to setup the database.
# It may prompt you to make a super user, do it! Use the same email as you put in the config file for your admin user
# This creates the schemas
./manage.py migrate
# manually change portal_ipaddress.ip to type "inet"
sudo su postgres
psql -d portal
ALTER TABLE portal_ipaddress ALTER COLUMN ip TYPE inet USING(ipinet);
\q
exit
Next you need to fix the indexing. It will look somethiing like this. You need to find the index on ‘ip varchar_patter_ops’.
portal=# select indexname, indexdef from pg_indexes where tablename = 'portal_ipaddress';
indexname | indexdef
-----------------------------------+----------------------------------------------------------------------------------------------------------------
portal_ipaddress_pkey | CREATE UNIQUE INDEX portal_ipaddress_pkey ON public.portal_ipaddress USING btree (id)
portal_ipaddress_ip_key | CREATE UNIQUE INDEX portal_ipaddress_ip_key ON public.portal_ipaddress USING btree (ip)
portal_ipaddress_ip_0d291e7e_like | CREATE INDEX portal_ipaddress_ip_0d291e7e_like ON public.portal_ipaddress USING btree (ip varchar_pattern_ops)
portal_ipaddress_host_id_0bfe4212 | CREATE INDEX portal_ipaddress_host_id_0bfe4212 ON public.portal_ipaddress USING btree (host_id)
(4 rows)
portal=# drop index if exists portal_ipaddress_ip_0d291e7e_like;
DROP INDEX
portal=# ALTER TABLE portal_ipaddress ALTER COLUMN ip TYPE inet USING (ip::inet);
ALTER TABLE
Test it out! Open your ip:8000 to verify that the install worked. If so, let’s move on to setting up uwsgi and nginx
./manage.py runserver 0.0.0.0:8000
Set up an inital superuser on the portal.
You will need this to login to an initially empty portal instance. If you are importing an existing portal database, then this step can be skipped.
./manage.py createsuperuser
./manage.py runserver 0.0.0.0:8000
# login and see that is functions as expected
Now we setup the webserver component.
sudo vi /etc/nginx/nginx.conf
You’ll almost certainly need to uncomment the following line:
server_names_hash_bucket_size 64;
And save the file (wq! if using vi). Now to move the uwsgi/nginx configs in place. Either install either old style init script or systemd config:
cd /var/www/dns-portal
sudo cp ./deploy/uwsgi.conf /etc/init/
or for systems using systemd:
cd /var/www/dns-portal
sudo cp ./deploy/emperor.uwsgi.service /etc/systemd/system/emperor.uwsgi.service
Set up SSL certificates
This can either be with LetEncrypt (certbot) or using certs signed by another certificate provider. If using LetsEncrypt’s certbot (encouraged), just follow their instructions being sure to list all the domains you are likely to be accessed as. Here are the instructions for creating a certificate signing request and adding the resulting certificate to the system. Certbot will do this automatically for nginx:
sudo mkdir /etc/nginx/ssl
sudo openssl req -new -newkey rsa:2048 -nodes -keyout /etc/nginx/ssl/dns-portal.key -out /etc/nginx/ssl/dns-portal.csr #Generate a CSR for your https
# Fill in the details!
sudo view /etc/nginx/ssl/dns-portal.csr # Submit your CSR to get the cert signed for web ssl; startssl is free!
sudo vi /etc/nginx/ssl/dns-portal.crt #Put your signed certificate (which you probably got from startssl) in here
Next, set up the logging and start the services. uwsgi will run as user pdns and group portal. Its createa a socket in /var/www/dns-portal, so we need to adjust the permissions according.
# setup uwsgi
chmod 775 /var/www/dns-portal
sudo mkdir -p /var/log/uwsgi
sudo touch /var/log/uwsgi/portal.log
sudo chown portal:portal /var/log/uwsgi/portal.log
sudo systemctl enable emperor.uwsgi
sudo systemctl start emperor.uwsgi
# and nginx
sudo cp ./deploy/portal_nginx.conf /etc/nginx/sites-available/portal.conf
sudo ln -s /etc/nginx/sites-available/portal.conf /etc/nginx/sites-enabled/
sudo service nginx restart
Backup
One potential scheme is to use crontab to make a backup and manage the files. Also consider database replication which you may choose to do anyways to enable additional authoritative DNS servers.
# m h dom mon dow command
# hourly pg backup
29 * * * * /usr/bin/sudo -u postgres /usr/bin/pg_dumpall | /bin/gzip > /home/tom/pgbackup/pg_dump_`/bin/date +\%Y-\%m-\%d_\%H\%M\%S`.sql.gz
# retain 1 monthly pg backup after 60 days
32 2 * * * find /home/tom/pgbackup/ -name 'pg_dump_????-??-01_00*' -prune -o -mtime +60 -exec rm {} \;
# retain 1 daily pg backup after 10 days
33 2 * * * find /home/tom/pgbackup/ -name 'pg_dump_????-??-??_00*' -prune -o -mtime +10 -exec rm {} \;
Set up a secondary server with powerdns and optionally the portal code
Its important to not add the anycast address for powerdns until after the replication is verified working as intended. Initially, configure powerdns to server from 127.0.0.2 so you have some means of testing. You can also configure this server to run unbound if you choose.
Changes to Primary to enable publishing
# On Primary as postgres
psql <<DONE
CREATE USER srv1_westin REPLICATION LOGIN ENCRYPTED PASSWORD 'myreplicationpassword';
GRANT CONNECT ON DATABASE powerdns TO srv1_westin;
GRANT CONNECT ON DATABASE portal TO srv1_westin;
DONE
psql -d powerdns <<DONE
GRANT USAGE ON SCHEMA public TO srv1_westin;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO srv1_westin;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO srv1_westin;
CREATE PUBLICATION powerdns_slot FOR ALL TABLES;
SELECT * FROM pg_create_logical_replication_slot('powerdns_slot', 'pgoutput');
DONE
psql -d portal <<DONE
GRANT USAGE ON SCHEMA public TO srv1_westin;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO srv1_westin;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO srv1_westin;
CREATE PUBLICATION portal_slot FOR ALL TABLES;
SELECT * FROM pg_create_logical_replication_slot('portal_slot', 'pgoutput');
DONE
pg_dump -d powerdns --schema-only > powerdns.schema
pg_dump -d portal --schema-only > portal.schema
Copy the schema files to /etc/postgresql/VERSION/main on the secondary server.
Update pg_hba.conf to add the replication user identity and access. To add a secondary on SRV1, with address 44.25.16.10, I created the following entry at the end of /etc/postgresql/VERSION/main/pg_hba.conf:
host all srv1_westin 44.25.16.10/32 md5
Changes to Secondary
Before you can start logical replication by adding subscriptions, you need to initialize the datables. This will be done by loading the schema files you dumped on the Primary.
# On Secondary as postgres
psql <<DONE
CREATE DATABASE powerdns;
CREATE DATABASE portal;
CREATE USER pdns;
CREATE USER portal;
DONE
psql -d powerdns -f /tmp/powerdns.schema
psql -d portal -f /tmp/portal.schema
psql -d powerdns <<DONE
CREATE SUBSCRIPTION powerdns_sub CONNECTION 'host=srv2.ziply.hamwan.net port=5432 user=srv1_westin dbname=powerdns password=LXot6oyfV798IN5T'
PUBLICATION powerdns_slot;
DONE
psql -d portal <<DONE
CREATE SUBSCRIPTION portal_sub CONNECTION 'host=srv2.ziply.hamwan.net port=5432 user=srv1_westin dbname=portal password=LXot6oyfV798IN5T'
PUBLICATION portal_slot;
DONE
Assumptions
This is for an older system with quagga based OSPF.
Install ntp
sudo apt-get install ntp
Configure ntp
sudo cat >> /etc/ntp.conf
driftfile /var/lib/ntp/ntp.drift
statistics loopstats peerstats clockstats
filegen loopstats file loopstats type day enable
filegen peerstats file peerstats type day enable
filegen clockstats file clockstats type day enable
# time.k7nvh.hamwan.net
server 44.24.255.3 iburst prefer
# time02.k7nvh.hamwan.net
server 44.24.255.5 prefer
# ntp.snodem.hamwan.net
server 44.25.142.128 prefer
server 0.us.pool.ntp.org
server 1.us.pool.ntp.org
server 2.us.pool.ntp.org
server 3.us.pool.ntp.org
restrict -4 default kod notrap nomodify nopeer noquery
restrict -6 default kod notrap nomodify nopeer noquery
restrict 127.0.0.1
restrict 1
<CTRL-D>
Configure NTP anycast interface
sudo cat >> /etc/network/interfaces
auto any-ntp
iface any-ntp inet manual
pre-up ip tuntap add dev any-ntp mode tap
pre-up ip l set dev any-ntp mtu 1418
post-up ip a add <ANYCAST IP 1>/32 dev any-ntp
post-up ip a add <ANYCAST IP 2>/32 dev any-ntp
post-up ip l set dev any-ntp up
post-up service ntp restart
post-down ip tuntap del dev any-ntp mode tap
<CTRL-D>
auto lo
iface lo inet loopback
post-up ip -6 a add <IPv6 ANYCAST IP 1>/128 dev lo
post-up ip -6 a add <IPv6 ANYCAST IP 2>/128 dev lo
Start the NTP anycast service
sudo ifup lo
sudo ifup any-ntp
Verify functionality
ip a # Should see the any-ntp interface with the two anycast IPs
ntpdate -d <ANYCAST IP 1> # Should see the current ntp date information
ntpdate -d <IPv6 ANYCAST IP 1> # Same as IPv4
Verify the service is being advertised to OSPF
ssh <NEAREST OSPF ROUTER>
/ip route check <ANYCAST IP 1> # Should display nearest server's primary IP as nexthop
/ipv6 route check <IPv6 ANYCAST IP 1> # Should display Link Local address of local server's ethernet interface
Assumptions
As implemented in May 2023 on a current Debian image using chrony. New install of Debian 11 (bullseye) on a small server (real or VM).
Install ntp and frr routing software
sudo apt-get install chrony frr
Disable DHCP driven ntp configuration (do not request ntp-servers)
sudo cat > /etc/dhcp/dhclient.conf <<EOF
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
send host-name = gethostname();
request subnet-mask, broadcast-address, time-offset, routers,
domain-name, domain-name-servers, domain-search, host-name,
dhcp6.name-servers, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers,
netbios-name-servers, netbios-scope, interface-mtu,
rfc3442-classless-static-routes;
EOF
Configure frr (OSPF)
sudo rm /etc/frr/frr.conf
sed -i -e 's/ospfd=no/ospfd=yes/' -e 's/ospf6d=no/ospf6d=yes/' /etc/frr/daemons
cat > /etc/frr/ospfd.conf <<EOF
password PASSWORD
enable password PASSWORD
log file /var/log/frr/ospfd.log
interface ens18
ip ospf authentication message-digest
ip ospf message-digest-key 1 md5 ABCDEFABCD
ip ospf priority 10
router ospf
ospf router-id 44.25.12.75
redistribute connected
distribute-list AMPR out connected
network 44.25.142.0/24 area 0.0.0.0
network 44.24.244.0/23 area 0.0.0.0
network 44.25.0.0/22 area 0.0.0.0
area 0 authentication message-digest
access-list AMPR permit 44.0.0.0/9
access-list AMPR permit 44.128.0.0/10
EOF
cat > /etc/frr/ospf6d.conf <<EOF
password PASSWORD
enable password PASSWORD
log file /var/log/frr/ospf6d.log
interface ens18
ipv6 ospf6 priority 10
interface lo
router ospf6
router-id 44.25.12.75
redistribute connected
interface eth0 area 0.0.0.0
interface lo area 0.0.0.0
area 0.0.0.0 range 2604:5000:20:1::4/128
area 0.0.0.0 range 2604:5000:20:2::4/128
EOF
cat > /etc/frr/zebra.conf <<EOF
password PASSWORD
enable password PASSWORD
logfile /var/log/frr/zebra.log
EOF
Configure chrony
sudo cat > /etc/chrony/chrony.conf <<EOF
# Welcome to the chrony configuration file. See chrony.conf(5) for more
# information about usable directives.
# Include configuration files found in /etc/chrony/conf.d.
confdir /etc/chrony/conf.d
# time.k7nvh.hamwan.net
server 44.24.255.3 prefer
# time02.k7nvh.hamwan.net
server 44.24.255.5 prefer
# ntp.snodem.hamwan.net
server 44.25.142.128 prefer
server 0.us.pool.ntp.org
server 1.us.pool.ntp.org
server 2.us.pool.ntp.org
# Use NTP sources found in /etc/chrony/sources.d.
sourcedir /etc/chrony/sources.d
# This directive specify the location of the file containing ID/key pairs for
# NTP authentication.
keyfile /etc/chrony/chrony.keys
# This directive specify the file into which chronyd will store the rate
# information.
driftfile /var/lib/chrony/chrony.drift
# Uncomment the following line to turn logging on.
log tracking measurements statistics
# Save NTS keys and cookies.
ntsdumpdir /var/lib/chrony
# Log files location.
logdir /var/log/chrony
# Stop bad estimates upsetting machine clock.
maxupdateskew 100.0
# This directive enables kernel synchronisation (every 11 minutes) of the
# real-time clock. Note that it can't be used along with the 'rtcfile' directive.
rtcsync
# Step the system clock instead of slewing it if the adjustment is larger than
# one second, but only in the first three clock updates.
makestep 1 3
# Get TAI-UTC offset and leap seconds from the system tz database.
# This directive must be commented out when using time sources serving
# leap-smeared time.
leapsectz right/UTC
allow 44.0.0.0/9
allow 44.128.0.0/10
EOF
Configure NTP anycast interface addresses to loopback interface
Add anycast addresses after ‘iface lo inet loopback’ (note indent):
iface lo inet loopback
post-up ip a add <ANYCAST IP 1>/32 dev any-ntp
post-up ip a add <ANYCAST IP 2>/32 dev any-ntp
post-up ip -6 a add <IPv6 ANYCAST IP 1>/128 dev lo
post-up ip -6 a add <IPv6 ANYCAST IP 2>/128 dev lo
Restart routing and start the NTP anycast service
sudo ifdown lo
sudo ifup lo
sudo systemctl restart frr
sudo systemctl start chrony
Verify functionality
ip a # Should see the 4 anycast addresses on lo interface
sntp <ANYCAST IP 1> # Should see the current ntp date information
sntp <IPv6 ANYCAST IP 1> # Same as IPv4
Verify the service is being advertised to OSPF
ssh <NEAREST OSPF ROUTER>
/ip route check <ANYCAST IP 1> # Should display nearest server's primary IP as nexthop
/ipv6 route check <IPv6 ANYCAST IP 1> # Should display Link Local address of local server's ethernet interface