forked from DGNum/liminix
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
|
||||
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-rc-database = final.callPackage ./pkgs/s6-rc-database {};
|
||||
|
||||
dnsmasq = prev.dnsmasq.override {
|
||||
dbusSupport = false;
|
||||
};
|
||||
|
||||
pppoe = final.callPackage ./pkgs/pppoe {};
|
||||
ppp =
|
||||
(prev.ppp.override {
|
||||
|
|
|
@ -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;
|
||||
|
|
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
|
||||
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 ] ;
|
||||
}
|
||||
|
|
|
@ -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\"))'"
|
||||
|
|
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