commit c320d0afc712f54741182ecd583c20f8ede562c6
parent 6f23a456963c2fd11a8ad1a64457be86c2ffa1d5
Author: Daniel Barlow <dan@telent.net>
Date: Wed, 28 Sep 2022 21:33:18 +0100
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
Diffstat:
7 files changed, 175 insertions(+), 3 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)