From c320d0afc712f54741182ecd583c20f8ede562c6 Mon Sep 17 00:00:00 2001 From: Daniel Barlow Date: Wed, 28 Sep 2022 21:33:18 +0100 Subject: [PATCH] add dnsmasq and example config for it would be good to move more of this into a module, but that doesn't sit well with the (potential) ability to run more than one dnsmasq service, as modules are singletons --- THOUGHTS.txt | 13 ++++ overlay.nix | 4 ++ pkgs/liminix-tools/networking/default.nix | 3 + pkgs/liminix-tools/networking/dnsmasq.nix | 39 ++++++++++ tests/pppoe/configuration.nix | 25 ++++++- tests/pppoe/run.sh | 6 ++ tests/pppoe/test-dhcp-service.py | 88 +++++++++++++++++++++++ 7 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 pkgs/liminix-tools/networking/dnsmasq.nix create mode 100644 tests/pppoe/test-dhcp-service.py diff --git a/THOUGHTS.txt b/THOUGHTS.txt index 0b8c939..d8d2031 100644 --- a/THOUGHTS.txt +++ b/THOUGHTS.txt @@ -207,3 +207,16 @@ reference build-time packages, so we have x86-64 glibc in there We don't need syslog just to accommodate ppp, there's an underdocumented option for it to log to a file descriptor + +Wed Sep 28 16:04:02 BST 2022 + +Based on https://unix.stackexchange.com/a/431953 if we can forge +ethernet packets we might be able to write tests for e.g. "is the vm +running a dhcp server" + +Wed Sep 28 21:29:05 BST 2022 + +We can use Python "scapy" to generate dhcp request packets, and Python +'socket' model to send them encapsulated in UDP. Win + +It's extremely janky python diff --git a/overlay.nix b/overlay.nix index f37310f..693816e 100644 --- a/overlay.nix +++ b/overlay.nix @@ -12,6 +12,10 @@ final: prev: { s6-init-bin = final.callPackage ./pkgs/s6-init-bin {}; s6-rc-database = final.callPackage ./pkgs/s6-rc-database {}; + dnsmasq = prev.dnsmasq.override { + dbusSupport = false; + }; + pppoe = final.callPackage ./pkgs/pppoe {}; ppp = (prev.ppp.override { diff --git a/pkgs/liminix-tools/networking/default.nix b/pkgs/liminix-tools/networking/default.nix index 1592cb4..57694d7 100644 --- a/pkgs/liminix-tools/networking/default.nix +++ b/pkgs/liminix-tools/networking/default.nix @@ -18,6 +18,8 @@ in { name = "${interface.device}.addr.${address}"; up = "ip address add ${address}/${toString prefixLength} dev ${interface.device} "; down = "ip address del ${address}/${toString prefixLength} dev ${interface.device} "; + } // { + inherit (interface) device; }; udhcpc = callPackage ./udhcpc.nix {}; odhcpc = interface: { ... } @ args: longrun { @@ -25,6 +27,7 @@ in { run = "odhcpcd ${interface.device}"; }; pppoe = callPackage ./pppoe.nix {}; + dnsmasq = callPackage ./dnsmasq.nix {}; route = { name, target, via, dependencies }: oneshot { inherit name; diff --git a/pkgs/liminix-tools/networking/dnsmasq.nix b/pkgs/liminix-tools/networking/dnsmasq.nix new file mode 100644 index 0000000..a7c3403 --- /dev/null +++ b/pkgs/liminix-tools/networking/dnsmasq.nix @@ -0,0 +1,39 @@ +{ + liminix +, dnsmasq +, lib +}: +{ + user ? "dnsmasq" +, group ? "dnsmasq" +, interface +, upstreams ? [] +, ranges +, domain +} : +let + inherit (liminix.services) longrun; + inherit (lib) concatStringsSep; + name = "${interface.device}.dnsmasq"; +in longrun { + inherit name; + dependencies = [ interface ]; + run = '' + ${dnsmasq}/bin/dnsmasq \ + --user=${user} \ + --domain=${domain} \ + --group=${group} \ + --interface=${interface.device} \ + ${lib.concatStringsSep " " (builtins.map (r: "--dhcp-range=${r}") ranges)} \ + ${lib.concatStringsSep " " (builtins.map (r: "--server=${r}") upstreams)} \ + --keep-in-foreground \ + --dhcp-authoritative \ + --no-resolv \ + --log-dhcp \ + --enable-ra \ + --log-debug \ + --log-facility=- \ + --dhcp-leasefile=/run/${name}.leases \ + --pid-file=/run/${name}.pid + ''; +} diff --git a/tests/pppoe/configuration.nix b/tests/pppoe/configuration.nix index 2f8210a..811aa68 100644 --- a/tests/pppoe/configuration.nix +++ b/tests/pppoe/configuration.nix @@ -1,6 +1,6 @@ -{ config, pkgs, ... } : +{ config, pkgs, lib, ... } : let - inherit (pkgs.liminix.networking) interface address pppoe route; + inherit (pkgs.liminix.networking) interface address pppoe route dnsmasq; inherit (pkgs.liminix.services) oneshot longrun bundle target output; in rec { services.loopback = @@ -13,6 +13,10 @@ in rec { ]; }; + services.lan4 = + let iface = interface { type = "hardware"; device = "eth1";}; + in address iface { family = "inet4"; address ="192.168.19.1"; prefixLength = 24;}; + kernel.config = { "IKCONFIG_PROC" = "y"; "PPP" = "y"; @@ -53,14 +57,29 @@ in rec { dependencies = [iface]; }; + users.dnsmasq = { + uid = 51; gid= 51; gecos = "DNS/DHCP service user"; + dir = "/run/dnsmasq"; + shell = "/bin/false"; + }; + groups.dnsmasq = { + gid = 51; usernames = ["dnsmasq"]; + }; + services.dns = + dnsmasq { + interface = services.lan4; + ranges = ["192.168.19.10,192.168.19.253"]; + domain = "fake.liminix.org"; + }; + services.default = target { name = "default"; contents = with services; [ loopback defaultroute4 packet_forwarding + dns ]; }; - defaultProfile.packages = [ pkgs.hello ] ; } diff --git a/tests/pppoe/run.sh b/tests/pppoe/run.sh index 541b8a7..48cb80d 100755 --- a/tests/pppoe/run.sh +++ b/tests/pppoe/run.sh @@ -23,3 +23,9 @@ fi ../../scripts/run-qemu.sh --background foo.sock result/vmlinux result/squashfs nix-shell -p expect --run "expect getaddress.expect" + +set -o pipefail +response=$(nix-shell -p python3Packages.scapy --run 'python ./test-dhcp-service.py' ) + +echo "$response" +echo "$response" | nix-shell -p jq --run "jq -e 'select((.router == \"192.168.19.1\") and (.server_id==\"192.168.19.1\"))'" diff --git a/tests/pppoe/test-dhcp-service.py b/tests/pppoe/test-dhcp-service.py new file mode 100644 index 0000000..6f9ccaf --- /dev/null +++ b/tests/pppoe/test-dhcp-service.py @@ -0,0 +1,88 @@ +# forge packets for testing liminix and send them via the qemu udp +# multicast socket interface + +MCAST_GRP = '230.0.0.1' +MCAST_PORT = 1235 +MULTICAST_TTL = 2 + +TIMEOUT = 10 # seconds + +from warnings import filterwarnings +filterwarnings("ignore") + +import random +import binascii +import socket +import time +import json + +from builtins import bytes, bytearray + +from scapy.all import Ether, IP, UDP, BOOTP, DHCP, sendp, send, raw + +class JSONEncoderWithBytes(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, (bytes, bytearray)): + return obj.decode('utf-8') + return json.JSONEncoder.default(self, obj) + + +def dhcp_option(pkt, label): + if pkt.haslayer(DHCP): + for i in pkt[DHCP].options: + l, v = i + if l == label: + return v + return None + +def is_dhcp_offer(pkt): + val = dhcp_option(pkt, 'message-type') + return (val == 2) + + + +def mac_to_bytes(mac_addr: str) -> bytes: + """ Converts a MAC address string to bytes. + """ + return int(mac_addr.replace(":", ""), 16).to_bytes(6, "big") + + +client_mac = "01:02:03:04:05:06" +discover = ( + Ether(dst="ff:ff:ff:ff:ff:ff") / + IP(src="0.0.0.0", dst="255.255.255.255") / + UDP(sport=68, dport=67) / + BOOTP( + chaddr=mac_to_bytes(client_mac), + xid=random.randint(1, 2**32-1), + ) / + DHCP(options=[("message-type", "discover"), "end"]) +) +payload = raw(discover) + +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) +sock.settimeout(TIMEOUT) +sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, MULTICAST_TTL) + +sock.bind((MCAST_GRP, MCAST_PORT)) +host = socket.gethostbyname(socket.gethostname()) +sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(host)) +sock.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, + socket.inet_aton(MCAST_GRP) + socket.inet_aton(host)) + +endtime = time.time() + TIMEOUT +sock.sendto(payload, (MCAST_GRP, MCAST_PORT)) + +while time.time() < endtime: + try: + data, addr = sock.recvfrom(1024) + except socket.error: + print('Exception') + else: + reply = Ether(data) + if is_dhcp_offer(reply): + opts = dict([o for o in reply[DHCP].options if type(o) is tuple]) + print(json.dumps(opts, cls=JSONEncoderWithBytes)) + exit(0) +exit(1)