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
This commit is contained in:
parent
6f23a45696
commit
c320d0afc7
7 changed files with 175 additions and 3 deletions
13
THOUGHTS.txt
13
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
|
We don't need syslog just to accommodate ppp, there's an underdocumented
|
||||||
option for it to log to a file descriptor
|
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
|
||||||
|
|
|
@ -12,6 +12,10 @@ final: prev: {
|
||||||
s6-init-bin = final.callPackage ./pkgs/s6-init-bin {};
|
s6-init-bin = final.callPackage ./pkgs/s6-init-bin {};
|
||||||
s6-rc-database = final.callPackage ./pkgs/s6-rc-database {};
|
s6-rc-database = final.callPackage ./pkgs/s6-rc-database {};
|
||||||
|
|
||||||
|
dnsmasq = prev.dnsmasq.override {
|
||||||
|
dbusSupport = false;
|
||||||
|
};
|
||||||
|
|
||||||
pppoe = final.callPackage ./pkgs/pppoe {};
|
pppoe = final.callPackage ./pkgs/pppoe {};
|
||||||
ppp =
|
ppp =
|
||||||
(prev.ppp.override {
|
(prev.ppp.override {
|
||||||
|
|
|
@ -18,6 +18,8 @@ in {
|
||||||
name = "${interface.device}.addr.${address}";
|
name = "${interface.device}.addr.${address}";
|
||||||
up = "ip address add ${address}/${toString prefixLength} dev ${interface.device} ";
|
up = "ip address add ${address}/${toString prefixLength} dev ${interface.device} ";
|
||||||
down = "ip address del ${address}/${toString prefixLength} dev ${interface.device} ";
|
down = "ip address del ${address}/${toString prefixLength} dev ${interface.device} ";
|
||||||
|
} // {
|
||||||
|
inherit (interface) device;
|
||||||
};
|
};
|
||||||
udhcpc = callPackage ./udhcpc.nix {};
|
udhcpc = callPackage ./udhcpc.nix {};
|
||||||
odhcpc = interface: { ... } @ args: longrun {
|
odhcpc = interface: { ... } @ args: longrun {
|
||||||
|
@ -25,6 +27,7 @@ in {
|
||||||
run = "odhcpcd ${interface.device}";
|
run = "odhcpcd ${interface.device}";
|
||||||
};
|
};
|
||||||
pppoe = callPackage ./pppoe.nix {};
|
pppoe = callPackage ./pppoe.nix {};
|
||||||
|
dnsmasq = callPackage ./dnsmasq.nix {};
|
||||||
route = { name, target, via, dependencies }:
|
route = { name, target, via, dependencies }:
|
||||||
oneshot {
|
oneshot {
|
||||||
inherit name;
|
inherit name;
|
||||||
|
|
39
pkgs/liminix-tools/networking/dnsmasq.nix
Normal file
39
pkgs/liminix-tools/networking/dnsmasq.nix
Normal file
|
@ -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
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{ config, pkgs, ... } :
|
{ config, pkgs, lib, ... } :
|
||||||
let
|
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;
|
inherit (pkgs.liminix.services) oneshot longrun bundle target output;
|
||||||
in rec {
|
in rec {
|
||||||
services.loopback =
|
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 = {
|
kernel.config = {
|
||||||
"IKCONFIG_PROC" = "y";
|
"IKCONFIG_PROC" = "y";
|
||||||
"PPP" = "y";
|
"PPP" = "y";
|
||||||
|
@ -53,14 +57,29 @@ in rec {
|
||||||
dependencies = [iface];
|
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 {
|
services.default = target {
|
||||||
name = "default";
|
name = "default";
|
||||||
contents = with services; [
|
contents = with services; [
|
||||||
loopback
|
loopback
|
||||||
defaultroute4
|
defaultroute4
|
||||||
packet_forwarding
|
packet_forwarding
|
||||||
|
dns
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultProfile.packages = [ pkgs.hello ] ;
|
defaultProfile.packages = [ pkgs.hello ] ;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,3 +23,9 @@ fi
|
||||||
|
|
||||||
../../scripts/run-qemu.sh --background foo.sock result/vmlinux result/squashfs
|
../../scripts/run-qemu.sh --background foo.sock result/vmlinux result/squashfs
|
||||||
nix-shell -p expect --run "expect getaddress.expect"
|
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\"))'"
|
||||||
|
|
88
tests/pppoe/test-dhcp-service.py
Normal file
88
tests/pppoe/test-dhcp-service.py
Normal file
|
@ -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)
|
Loading…
Reference in a new issue