SOHO Router Setup Guide
Introduction
This is a small guide on how to setup a router.
Problem statement
Everyone uses some method to route internet traffic in their homes, businesses, and labs. However, given the current state of the world, we face challenges such as backdoors, poor security solutions, aggressive marketing, and overpriced products. These risks leave us with no choice but to take control of our own networking.
In this guide, we will configure our own router while keeping these challenges in mind.
Requirements
Our setup must meet the following criteria:
- Minimal attack surface
- Simple maintenance
- No unnecessary bloat
- Basic security enhancements
- Wi-Fi support
- IPv6 NAT
Overall strategy
We will not use existing solutions like OpenWrt or pfSense, as they contain too many components and have a larger attack surface.
For hardware, I chose the ZimaBoard because it has two Ethernet ports and AES-NI instructions, which may be useful for a future VPN setup. A Raspberry Pi could serve the same purpose, but it would require a USB Ethernet adapter.
Linux & Packages
We need to choose and install a Linux distribution. My personal preference is Void Linux with musl and runit.
- Runit is a simple and lightweight replacement for systemd.
- Musl is recommended for embedded systems due to its cleaner and simpler codebase.
Any other Linux distribution should work fine, especially if you already have a preferred one.
Packages that we’re going to be using:
nftables
- Firewallopenssh
- SSH serverdhcpcd
- DHCP client (to obtain an IP from the ISP)dnsmasq
- DHCP service for the local network and Wi-Fihostapd
- Wi-Fi access pointdnscrypt-proxy
- Secure DNSchrony
- Time synchronization service
Optional packages:
lighttpd
- HTTP for vnstat chartsvnstat
- Traffic usage monitor
Useful Networking Tools:
iperf3
nload
nmap
tcpdump
tcptrack
wavemon
traceroute
netcat
ifstatus
iptraf-ng
iftop
iputils
iw
sysstat
Wi-Fi Drivers (if needed):
wifi-firmware
wireless-regdb
Configuration files
Don’t forget to adjust interface names and IP nets
Static IPs for local net `rc.local`
File /etc/rc.local
#!/bin/sh
# Local net
ip link set dev enp3s0 up
ip addr add 192.168.190.1/24 brd + dev enp3s0
ip addr add fd11:1:1::1/64 dev enp3s0
ip link set enp3s0 mtu 9192
# ISP WAN
ifconfig enp2s0 mtu 9192
# Wi-Fi AP
ip addr add 192.168.191.1/24 dev wlp0s21f0u1
ip addr add fd11:1:2::1/64 dev wlp0s21f0u1
Kernel configuration and tcp ip forwarding `sysctl.conf`
File /etc/sysctl.conf
# Do not accept ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
# Do not accept source route
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
# Reverse path filter
# prevent some spoofing attacks
#net.ipv4.conf.all.rp_filter = 1
#net.ipv4.conf.default.rp_filter = 1
# Log Martian Packets
#net.ipv4.conf.all.log_martians = 1
#net.ipv4.conf.default.log_martians = 1
# Ignore IPv6 RA
#net.ipv6.conf.all.accept_ra = 0
#net.ipv6.conf.default.accept_ra = 0
kernel.sysrq=0
net.ipv4.tcp_timestamps=0
#net.ipv4.tcp_sack=1
net.core.netdev_max_backlog=10000
net.core.rmem_max=4194304
net.core.wmem_max=4194304
net.core.rmem_default=4194304
net.core.wmem_default=4194304
net.core.optmem_max=4194304
#net.ipv4.tcp_rmem="4096 87380 4194304"
#net.ipv4.tcp_wmem="4096 65536 4194304"
net.ipv4.tcp_low_latency=1
net.ipv4.tcp_adv_win_scale=1
# IP Forward
net.ipv4.ip_forward = 1
net.ipv4.ip_forward_update_priority = 1
net.ipv6.conf.all.forwarding=1
net.ipv4.tcp_syncookies=1
# Enable IPv6 Privacy Extensions
net.ipv6.conf.all.use_tempaddr = 2
net.ipv6.conf.default.use_tempaddr = 2
# No kernel dump
fs.suid_dumpable=0
kernel.core_pattern=|/bin/false
Stateful firewall `nftables.conf`
It drops invalid traffic while allowing established connections from the local network and Wi-Fi. Additionally, it permits certain IPv6 traffic from the ISP to establish an IPv6 connection and allows basic ICMP request/reply for monitoring the connection from the internet.
File /etc/nftables.conf
# enp2s0 - uplink
# enp3s0 - local net 192.168.190.0/24 fd11:1:1::/64
# wlp0s21f0u1 - wifi 192.168.191.0/24 fd11:1:2::/64
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state {established, related} counter accept comment "accept all connections related to connections made by us"
ct state invalid counter drop comment "early drop of invalid packets"
iif != lo ip daddr 127.0.0.1/8 counter drop comment "drop connections to loopback not coming from loopback"
iif != lo ip6 daddr ::1/128 counter drop comment "drop connections to loopback not coming from loopback"
tcp flags & (fin|syn|rst|ack) != syn ct state new counter drop
tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg counter drop
tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop
tcp flags syn tcp option maxseg size 1-536 counter drop
iif enp2s0 ip saddr @snort_block log prefix "nft: snort_block " counter drop
iif lo accept comment "accept loopback"
# iif enp2s0 tcp dport 2233 ct state new limit rate 5/minute counter accept comment "accept inet SSH"
iif {enp3s0, wlp0s21f0u1} tcp dport 2233 counter accept comment "accept local SSH"
iif wlp0s21f0u1 accept comment "local wifi"
iif enp3s0 accept comment "local lan"
iif enp2s0 ip6 daddr fe80::/6 udp dport 546 counter accept
iif enp2s0 ip6 daddr fe80::/6 udp dport 547 counter accept
iif enp2s0 icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } ip6 saddr fe80::/10 counter accept comment "ipv6"
meta l4proto icmp icmp type echo-request limit rate over 10/second burst 4 packets counter drop comment "No ping floods"
meta l4proto ipv6-icmp icmpv6 type echo-request limit rate over 10/second burst 4 packets counter drop comment "No ping floods"
ip protocol icmp icmp type {echo-request, echo-reply} counter accept comment "accept all ICMP types"
icmpv6 type {echo-request, echo-reply} counter accept comment "accept req/reply ICMP types"
iif enp2s0 counter drop comment "count wan drops"
counter comment "count dropped packets"
}
set snort_block {
type ipv4_addr; flags dynamic;flags interval;
elements = {
163.0.0.0/8,
157.185.128.0/18,
138.113.0.0/16,
14.0.0.0/16
}
}
chain forward {
type filter hook forward priority 0; policy drop;
oif enp2s0 udp dport 53 counter drop comment "don't leak DNS"
oif enp2s0 tcp dport 53 counter drop comment "don't leak DNS"
ip saddr != 192.168.190.0/24 iifname "enp3s0" counter drop comment "unknown lan src"
ip saddr != 192.168.191.0/24 iifname "wlp0s21f0u1" counter drop comment "unknown wifi src"
ip6 saddr != fd11:1:1::/64 iifname "enp3s0" counter drop comment "unknown lan src"
ip6 saddr != fd11:1:2::/64 iifname "wlp0s21f0u1" counter drop comment "unknown wifi src"
iif enp2s0 oif enp3s0 ct state related,established counter accept comment "wan -> local"
iif enp3s0 oif enp2s0 counter accept comment "local -> wan"
iif enp2s0 oif wlp0s21f0u1 ct state related,established counter accept comment "wan -> wifi"
iif wlp0s21f0u1 oif enp2s0 counter accept comment "wifi -> wan"
iif wlp0s21f0u1 oif enp3s0 counter accept comment "wifi -> lan"
iif enp3s0 oif wlp0s21f0u1 counter accept comment "lan -> wifi"
counter comment "count dropped packets"
}
chain output {
type filter hook output priority 0; policy accept;
oif enp2s0 tcp dport 53 counter drop comment "no DNS"
oif enp2s0 udp dport 53 counter drop comment "no DNS"
oif lo counter accept comment "count lo"
counter comment "count accepted packets"
}
}
table inet nat {
chain prerouting {
type nat hook prerouting priority 0; policy accept;
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oif enp2s0 masquerade
}
}
SSH
Let’s use a non-default SSH port, adjust ciphers/MACs/KexAlgorithms security settings and disable password authentication.
Put your public ssh key into /root/.ssh/authorized_keys
.
File /etc/ssh/sshd_config
Port 2233
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
PermitRootLogin prohibit-password
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
UseDNS no
Subsystem sftp /usr/libexec/sftp-server
Uplink DHCP client IPv4+IPv6
There are many ways to set up IPv6, but we will use NAT even for IPv6. This means the router will request only a single IPv6 address instead of an entire subnet. The IPv4 configuration follows the standard approach.
File /etc/dhcpcd.conf
noipv4ll
controlgroup wheel
noipv6rs
noarp
noauthrequired
persistent
nohook hostname resolv.conf timesyncd.conf ntp.conf
#debug
#logfile /var/log/dhcpcd.log
#option domain_name_servers, host_name, interface_mtu, classless_static_routes
require dhcp_server_identifier
vendorclassid
allowinterfaces enp2s0
interface enp2s0
iaid 01:08:00:01
ipv6rs
ipv6
# ia_pd 01:08:00:01 enp2s0//64 # in case if you want to request a //64 net from the ISP
ia_na
clientid
vendorclassid
broadcast
option subnet_mask routers domain_name_servers domain_name broadcast_address dhcp_lease_time dhcp_renewal_time dhcp_rebinding_time sip_server
option dhcp6_vivso dhcp6_name_servers dhcp6_domain_search #dhcp6_auth
Local net DHCP server IPv4+IPv6
/etc/dnsmasq.conf
port=0
edns-packet-max=4096
interface=enp3s0
interface=wlp0s21f0u1
except-interface=enp2s0
no-dhcp-interface=enp2s0
no-hosts
enable-ra
dhcp-authoritative
strict-order
dhcp-range=enp3s0,192.168.190.50, 192.168.190.250, 48h
dhcp-option=enp3s0,option:netmask,255.255.255.0
dhcp-option=enp3s0,option:router,192.168.190.1
dhcp-option=enp3s0,option:dns-server,192.168.190.1
dhcp-option=enp3s0,option:ntp-server,192.168.190.1
dhcp-option=enp3s0,option:mtu,9192
dhcp-range=enp3s0, fd11:1:1::200, fd11:1:1::f00, 64, 48h
dhcp-range=enp3s0, fd11:1:1::, ra-stateless, slaac, 48h
dhcp-option=enp3s0, option6:dns-server,[fd11:11:1::1]
dhcp-option=enp3s0,option6:information-refresh-time,1h
dhcp-range=wlp0s21f0u1,192.168.191.50, 192.168.91.250, 48h
dhcp-option=wlp0s21f0u1,option:netmask,255.255.255.0
dhcp-option=wlp0s21f0u1,option:router,192.168.191.1
dhcp-option=wlp0s21f0u1,option:dns-server,192.168.191.1
dhcp-option=wlp0s21f0u1,option:ntp-server,192.168.191.1
dhcp-option=wlp0s21f0u1,option:mtu,1492
dhcp-range=wlp0s21f0u1, fd11:1:2::200, fd11:11:2::f00, 64, 48h
dhcp-range=wlp0s21f0u1, fd11:1:2::, ra-stateless, slaac, 48h
dhcp-option=wlp0s21f0u1, option6:dns-server,[fd11:1:2::1]
dhcp-option=wlp0s21f0u1, option6:information-refresh-time,1h
#dhcp-host=08:bf:b1:11:11:11,192.168.190.15,[fd11:1:1::175],ml-lab # static IPs
#dhcp-host=38:87:d1:11:11:11,192.168.191.200,[fd11:1:2::200],lemur # static IPs
dhcp-leasefile=/var/dnsmasq.leases
dhcp-authoritative
dhcp-rapid-commit
log-dhcp
dhcp-name-match=set:wpad-ignore,wpad
dhcp-ignore-names=tag:wpad-ignore
Hidden WiFi 5GHz WPA3
Depends on your Wi-Fi dongle capabilities you may need to switch from 5GHz to 2.4GHz. I’m using PAU0D AC1200
dongle.
/etc/hostapd/hostapd.conf
interface=wlp0s21f0u1
ssid2="CHANGE_SSID_HERE"
bssid=ff:ff:ff:ff:00:00
country_code=US
max_num_sta=255
ignore_broadcast_ssid=1
multi_ap=0
beacon_int=500
obss_interval=0
dtim_period=2
hw_mode=a
channel=149
ieee80211h=1
ieee80211d=1
ieee80211n=1
ieee80211ac=1
ieee80211w=2
wmm_enabled=1
ht_capab=[HT40+][HT40-][SHORT-GI-20][SHORT-GI-40][LDPC]
vht_oper_chwidth=1
vht_oper_centr_freq_seg0_idx=155
vht_capab=[SHORT-GI-80][TX-STBC-2BY1]
own_ip_addr=127.0.0.1
eapol_key_index_workaround=0
eap_server=0
auth_algs=1
wpa=2
wpa_passphrase=CHANGE_PASSWORD_HERE
wpa_key_mgmt=SAE
wpa_pairwise=CCMP
rsn_pairwise=CCMP
logger_stdout=-1
logger_stdout_level=2
ctrl_interface=/var/run/hostapd
ctrl_interface_group=0
Local net DNS server and DNS blackhole
/etc/dnscrypt-proxy/dnscrypt-proxy.toml
listen_addresses = ['127.0.0.1:53', '127.0.2.1:53', '192.168.190.1:53','192.168.191.1:53','[fd11:1:1::1]:53', '[fd11:1:2::1]:53', '[::1]:53']
max_clients = 250
ipv4_servers = true
ipv6_servers = true
dnscrypt_servers = true
doh_servers = true
require_dnssec = false
require_nolog = true
require_nofilter = true
disabled_server_names = []
force_tcp = false
timeout = 5000
keepalive = 30
log_file = '/var/log/dnscrypt-proxy/dnscrypt-proxy.log'
cert_refresh_delay = 240
fallback_resolvers = ['9.9.9.9:53', '8.8.8.8:53']
ignore_system_dns = true
netprobe_timeout = 60
netprobe_address = '9.9.9.9:53'
log_files_max_size = 10
log_files_max_age = 7
log_files_max_backups = 5
block_ipv6 = false
block_unqualified = true
block_undelegated = true
reject_ttl = 600
cloaking_rules = 'blocked'
cache = true
cache_size = 8192
cache_min_ttl = 43200
cache_max_ttl = 86400
cache_neg_min_ttl = 300
cache_neg_max_ttl = 3600
[local_doh]
[query_log]
file = '/var/log/dnscrypt-proxy/dnscrypt-query.log'
format = 'tsv'
ignored_qtypes = ['DNSKEY', 'NS', 'AAAA']
[nx_log]
file = '/var/log/dnscrypt-proxy/dnscrypt-nx.log'
format = 'tsv'
[blacklist]
[ip_blacklist]
[whitelist]
[schedules]
[sources]
[sources.'public-resolvers']
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md']
cache_file = 'public-resolvers.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
prefix = ''
[sources.'relays']
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/relays.md', 'https://download.dnscrypt.info/resolvers-list/v2/relays.md']
cache_file = 'relays.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
refresh_delay = 72
prefix = ''
[broken_implementations]
broken_query_padding = ['cisco', 'cisco-ipv6', 'cisco-familyshield']
[anonymized_dns]
[static]
Update it on our taste, there is a bunch of DNS black lists on github.
/etc/dnscrypt-proxy/blocked
dc.services.visualstudio.com 192.168.190.1
izatcloud.net 192.1681.90.1
rogapi.asus.com 192.168.190.1
cxcs.microsoft.net 192.168.190.1
classify-client.services.mozilla.com 192.168.190.1
merino.services.mozilla.com 192.168.190.1
location.services.mozilla.com 192.168.190.1
getpocket.cdn.mozilla.net 192.168.190.1
firefox-api-proxy.cdn.mozilla.net 192.168.190.1
contile.services.mozilla.com 192.168.190.1
incoming.telemetry.mozilla.org 192.168.190.1
client.wns.windows.com 192.168.190.1
api.msn.com 192.168.190.1
prod.detectportal.prod.cloudops.mozgcp.net 192.168.190.1
www.msftconnecttest.com 192.168.190.1
firefox.settings.services.mozilla.com 192.168.190.1
push.services.mozilla.com 192.168.190.1
Local time server
/etc/chrony.conf
pool pool.ntp.org iburst xleave
driftfile /var/lib/chrony/drift
makestep 0.1 3
rtcsync
allow 192.168.190.0/24
allow 192.168.191.0/24
allow fd11:1:1::/64
allow fd11:1:2::/64
Local HTTP server for static content
We will generate net usage stats from vnstat
and put them into /var/www/html
/etc/lighttpd/lighttpd.conf
server.modules = (
"mod_indexfile",
"mod_access",
)
server.document-root = "/var/www/html"
server.upload-dirs = ( "/var/cache/lighttpd/uploads" )
server.errorlog = "/var/log/lighttpd/error.log"
server.pid-file = "/run/lighttpd.pid"
server.username = "_lighttpd"
server.groupname = "_lighttpd"
server.port = 80
server.feature-flags += ("server.h2proto" => "disable")
server.feature-flags += ("server.h2c" => "disable")
server.feature-flags += ("server.graceful-shutdown-timeout" => 5)
server.http-parseopts = (
)
index-file.names = ( "index.html" )
url.access-deny = ( "~", ".inc" )
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
include_shell "/usr/share/lighttpd/create-mime.conf.pl"
server.modules += (
"mod_staticfile",
)
/var/www/html/index.html
<html>
<head>
</head>
<body style='background-color:#000;color:#90A090'>
<h1>wan</h1>
<img src="wan_hs.png" alt="stat" /><br/>
<img src="wan_d.png" alt="stat" />
<h1>lan</h1>
<img src="lan_hs.png" alt="stat" /><br/>
<img src="lan_d.png" alt="stat" />
<h1>wifi</h1>
<img src="wifi_hs.png" alt="stat" /><br/>
<img src="wifi_d.png" alt="stat" />
</body>
</html>
Network usage statistics
/etc/vnstat.conf
Interface ""
DatabaseDir "/var/lib/vnstat"
Locale "-"
DayFormat "%Y-%m-%d"
MonthFormat "%Y-%m"
TopFormat "%Y-%m-%d"
RXCharacter "%"
TXCharacter ":"
RXHourCharacter "r"
TXHourCharacter "t"
UnitMode 0
RateUnit 1
RateUnitMode 1
OutputStyle 3
DefaultDecimals 2
HourlyDecimals 1
HourlySectionStyle 2
Sampletime 5
QueryMode 0
List5Mins 24
ListHours 24
ListDays 30
ListMonths 12
ListYears 0
ListTop 10
DaemonUser ""
DaemonGroup ""
BandwidthDetection 1
MaxBandwidth 100
5MinuteHours 48
HourlyDays 4
DailyDays 62
MonthlyMonths 25
YearlyYears -1
TopDayEntries 20
UpdateInterval 20
PollInterval 5
SaveInterval 5
OfflineSaveInterval 30
MonthRotate 1
MonthRotateAffectsYears 0
CheckDiskSpace 1
BootVariation 15
TrafficlessEntries 1
TimeSyncWait 5
BandwidthDetectionInterval 5
SaveOnStatusChange 1
UseLogging 2
CreateDirs 1
UpdateFileOwner 1
LogFile "/var/log/vnstat/vnstat.log"
PidFile "/run/vnstat/vnstat.pid"
64bitInterfaceCounters -2
DatabaseWriteAheadLogging 0
DatabaseSynchronous -1
HeaderFormat "%Y-%m-%d %H:%M"
HourlyRate 1
SummaryRate 1
SummaryLayout 1
TransparentBg 0
CBackground "000000"
CEdge "AEAEAE"
CHeader "606060"
CHeaderTitle "AFAFAF"
CHeaderDate "AFAFAF"
CText "90F090"
CLine "B0B0B0"
CLineL "-"
CRx "92CF00"
CTx "606060"
CRxD "-"
CTxD "-"
/etc/cron.daily/vnstatimages
#!/bin/sh
vnstati -hs -o /var/www/html/lan_hs.png -i enp3s0 -ne
vnstati -hs -o /var/www/html/wan_hs.png -i enp2s0 -ne
vnstati -hs -o /var/www/html/wifi_hs.png -i wlp0s21f0u1 -ne
vnstati -d -o /var/www/html/lan_d.png -i enp3s0 -ne
vnstati -d -o /var/www/html/wan_d.png -i enp2s0 -ne
vnstati -d -o /var/www/html/wifi_d.png -i wlp0s21f0u1 -ne
Future Improvements
- VPN server
- IPS/IDS
- File system integrity monitoring
- Email notifications
- CVE monitoring for tools used by the router
After party
I hope this guide helps you set up a router that you have full control over and that gives you confidence. I’d love to hear your feedback and improve the guide further.
Thank you!
P.S.: Good old human paranoia never fails.