Erik HeimdalEH
BlogResuméProjects

Network perfection

5 minute read

Shortly after moving to Lund I found out that internet was not included in the rent, and after ordering internet from an external provider I didn't get any router. This presented a great opportunity to create the perfect local area network.

Tools Used

Method

Choosing the hardware

I had an old laptop with an ethernet port that I was going to use as a general server anyway, so I thought why not convert it to a router instead of buying a new. After all, there cannot be that much of a difference between a "real" routers network card, and a general computers network card. I had debian installed on the server, and as linux has great networking support for other reasons there were great premade tools for this.

Configuring DNS

I used dnsmasq for DNS and DHCP and configured it with the following options:

interface=NETWORK_CARD_HERE

cache-size=1024     # Cache 1024 DNS requests
dhcp-authoritative  # Assign router as DHCP server
dnssec              # Enable DNS response versification
dhcp-option=option:dns-server,10.10.10.10   # Assign router as DNS server
dhcp-option=option:domain-name,lan          # Assign domain name for DHCP clients

# Disable DNS handling, as cloudflared handles it with DoH
no-resolv
server=127.0.0.1#5053

# Enable local DNS resolution
domain=lan
local=/lan/

There is no great way to configure a prime number DHCP network, but you can set the dhcp-range manually for each entry, which is what I did below.

# Configure IP space for DHCP to only use prime numbers
dhcp-range=10.1.1.1,10.1.1.1,255.0.0.0,infinite
dhcp-range=10.2.2.2,10.2.2.2,255.0.0.0,infinite
dhcp-range=10.3.3.3,10.3.3.3,255.0.0.0,infinite
dhcp-range=10.5.5.5,10.5.5.5,255.0.0.0,infinite
dhcp-range=10.7.7.7,10.7.7.7,255.0.0.0,infinite
dhcp-range=10.11.11.11,10.11.11.11,255.0.0.0,infinite
dhcp-range=10.13.13.13,10.13.13.13,255.0.0.0,infinite
dhcp-range=10.17.17.17,10.17.17.17,255.0.0.0,infinite
dhcp-range=10.19.19.19,10.19.19.19,255.0.0.0,infinite
dhcp-range=10.23.23.23,10.23.23.23,255.0.0.0,infinite
dhcp-range=10.29.29.29,10.29.29.29,255.0.0.0,infinite
dhcp-range=10.31.31.31,10.31.31.31,255.0.0.0,infinite
dhcp-range=10.37.37.37,10.37.37.37,255.0.0.0,infinite
dhcp-range=10.41.41.41,10.41.41.41,255.0.0.0,infinite
dhcp-range=10.43.43.43,10.43.43.43,255.0.0.0,infinite
dhcp-range=10.47.47.47,10.47.47.47,255.0.0.0,infinite
dhcp-range=10.53.53.53,10.53.53.53,255.0.0.0,infinite
dhcp-range=10.59.59.59,10.59.59.59,255.0.0.0,infinite
dhcp-range=10.61.61.61,10.61.61.61,255.0.0.0,infinite
dhcp-range=10.67.67.67,10.67.67.67,255.0.0.0,infinite
dhcp-range=10.71.71.71,10.71.71.71,255.0.0.0,infinite
dhcp-range=10.73.73.73,10.73.73.73,255.0.0.0,infinite
dhcp-range=10.79.79.79,10.79.79.79,255.0.0.0,infinite
dhcp-range=10.83.83.83,10.83.83.83,255.0.0.0,infinite
dhcp-range=10.89.89.89,10.89.89.89,255.0.0.0,infinite
dhcp-range=10.97.97.97,10.97.97.97,255.0.0.0,infinite
dhcp-range=10.101.101.101,10.101.101.101,255.0.0.0,24h
dhcp-range=10.103.103.103,10.103.103.103,255.0.0.0,24h
dhcp-range=10.107.107.107,10.107.107.107,255.0.0.0,24h
dhcp-range=10.109.109.109,10.109.109.109,255.0.0.0,24h
dhcp-range=10.113.113.113,10.113.113.113,255.0.0.0,24h
dhcp-range=10.127.127.127,10.127.127.127,255.0.0.0,24h
dhcp-range=10.131.131.131,10.131.131.131,255.0.0.0,24h
dhcp-range=10.137.137.137,10.137.137.137,255.0.0.0,24h
dhcp-range=10.139.139.139,10.139.139.139,255.0.0.0,24h
dhcp-range=10.149.149.149,10.149.149.149,255.0.0.0,24h
dhcp-range=10.151.151.151,10.151.151.151,255.0.0.0,24h
dhcp-range=10.157.157.157,10.157.157.157,255.0.0.0,24h
dhcp-range=10.163.163.163,10.163.163.163,255.0.0.0,24h
dhcp-range=10.167.167.167,10.167.167.167,255.0.0.0,24h
dhcp-range=10.173.173.173,10.173.173.173,255.0.0.0,24h
dhcp-range=10.179.179.179,10.179.179.179,255.0.0.0,24h
dhcp-range=10.181.181.181,10.181.181.181,255.0.0.0,24h
dhcp-range=10.191.191.191,10.191.191.191,255.0.0.0,24h
dhcp-range=10.193.193.193,10.193.193.193,255.0.0.0,24h
dhcp-range=10.197.197.197,10.197.197.197,255.0.0.0,24h
dhcp-range=10.199.199.199,10.199.199.199,255.0.0.0,24h
dhcp-range=10.211.211.211,10.211.211.211,255.0.0.0,24h
dhcp-range=10.223.223.223,10.223.223.223,255.0.0.0,24h
dhcp-range=10.227.227.227,10.227.227.227,255.0.0.0,24h
dhcp-range=10.229.229.229,10.229.229.229,255.0.0.0,24h
dhcp-range=10.233.233.233,10.233.233.233,255.0.0.0,24h
dhcp-range=10.239.239.239,10.239.239.239,255.0.0.0,24h
dhcp-range=10.241.241.241,10.241.241.241,255.0.0.0,24h
dhcp-range=10.251.251.251,10.251.251.251,255.0.0.0,24h

Apparently I included 1 there aswell. Not sure why, as its not prime, but I like 1 so I'll keep it anyway.

Cloudflared with DoH

I decided to use cloudflares tool cloudflared for DNS lookups locally on the router. I like this approach as all requests will be encrypted now, usually that is quite trubblesome to achieve on a network. I guess the downside is that this requires another request from dnsmasq to the cloudflared service, and also that DoH has some overhead (because of the handshakes and TCP stuff) which makes it slightly slower. In general I don't think this is much of a problem, especially as DNS responses are usually cached on the device or the router anyway.

Here is the config I used for cloudflared:

proxy-dns: true
proxy-dns-port: 5053
proxy-dns-address: 0.0.0.0
proxy-dns-upstream:
  - https://1.1.1.1/dns-query
  - https://1.0.0.1/dns-query

Setting up the AP

I chose not to use WPA 3 as I want crosscommunication, and the option of interception. Also, I trust the devices on LAN anyway.

Here is the config I used for hostapd (/etc/hostapd/hostapd.conf):

interface=WIRELESS_INTERFACE
ssid=WIFI_NAME
hw_mode=g
channel=6
ieee80211n=1
wmm_enabled=1
ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40]
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_passphrase=WIFI_PASSWORD
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP

Setting up mining network

As I have controll over the router, it makes it easier to recognize when my phone joins/leaves the network. All device association/disassociation events are logged in /var/log/syslog. Even if the device does not actively send a disassociation packet but still becomes idle, such as if the device runns out of battery, or simply leaved the LAN physically, the device will be flagged as disassociated after a few minutes. This is great, but also creates some false positives when the phone is simply inactive (mainly during night). For this reason the router will try to ping my phone aswell, to assure that it really isn't connected anymore.

After my phone (and persumably me aswell) has left the network, the router will broadcast to all other devices that its mining time over UDP. If any device misses the initial broadcast, the status will continue to be re-sent every 5 minutes. If any miner would boot after my devices have left, they can ask the router for the mining status. The router also sends Wake on WLAN (WoWLAN) packets to all preconfigured computers that should wake up and start mining when I leave. This primairly includes my main PC, which isn't turned on all the time because of fan noise.

All mining devices on the network (including the router) will then start a local instance of a monero miner, working tirelessly while I'm away from home.

I don't pay for electricity, and the fan noise does not bother me while I'm away from home. As a bonus my room is nice and warm when I come back from university in the winter. The network does not generate any remarkable revenue though, but in any case there is really no downside to this method.

Here is the code for the mining client:

import socket
import subprocess

from lib import query_miner_status


def udp_listener(port: int):
    """
    Listen for UDP broadcasts on a specific port and execute or terminate mxrig based on the received message.
    """
    # Create a UDP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # Bind the socket to the port
    sock.bind(("", port))

    # Check with network if xmrig should be running, default to False if no response
    active = query_miner_status("10.255.255.255") == "START"

    print(f"Listening for UDP broadcasts on port {port}...")

    while True:
        # Receive data from the socket (up to 1024 bytes)
        data, addr = sock.recvfrom(1024)
        message = data.decode("utf-8")

        # Check if the message is 'START'
        if message == "START" and not active:
            active = True
            print(f"Received 'START' from {addr}. Launching xmrig...")
            subprocess.Popen(["sudo", "xmrig", "--config=/root/.xmrig.json"])
        elif message == "STOP" and active:
            active = False
            print(f"Received 'STOP' from {addr}. Terminating xmrig...")
            subprocess.run(["sudo", "pkill", "-9", "xmrig"])
        elif message == "STATUS":
            print(
                f"Received 'STATUS' from {addr}. xmrig is {'active' if active else 'inactive'}"
            )
            # Send the status back to the client
            sock.sendto(bytes("START" if active else "STOP", "utf-8"), addr)


if __name__ == "__main__":
    udp_listener(1337)

Here is the code for the mining server:

def main():
    global current_mode, timer

    run_subprocess(["tail", "-n0", "-f", "/var/log/syslog"])

    logging.info("Remember to run this as su!")

    associated_pattern = "IEEE 802.11: associated"
    disassociated_pattern = "IEEE 802.11: disassociated"

    try:
        p = subprocess.Popen(
            ["tail", "-n0", "-f", "/var/log/syslog"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )

        for line in iter(p.stdout.readline, ""):
            if associated_pattern in line and TARGET_DEVICE in line:
                logging.info("Target connected, broadcasting kill signal...")
                current_mode = MESSAGE.STOP
                send_udp_broadcast(MESSAGE.STOP, UDP_PORT)
            elif disassociated_pattern in line and TARGET_DEVICE in line and not ping_device(TARGET_DEVICE_IP):
                logging.info("Target disconnected, broadcasting start signal...")
                current_mode = MESSAGE.START
                send_udp_broadcast(MESSAGE.START, UDP_PORT)
                wake_network_devices()

            if timer is not None:
                timer.cancel()  # Cancel the previous timer
            broadcast_status_thread()  # Start a new timer

    except Exception as e:
        logging.error(f"An error occurred: {e}")


if __name__ == "__main__":
    main()

Result

The finnished network has a lot of great features.

It is actually really cool to see the network react to me comming home. It almost feels like the computers are alive, like in ToyStory when the toys only move when not observed. Anyway, I'm really happy with the setup and I think it is a great alternative to buying a router. I also think it is a great way to learn about networking, as you have to configure everything yourself.