Compare commits

...

2 Commits

Author SHA1 Message Date
Daniel Barlow c320d0afc7 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
2022-09-28 21:33:18 +01:00
Daniel Barlow 6f23a45696 configuration for users and groups 2022-09-28 21:31:15 +01:00
10 changed files with 219 additions and 5 deletions

View File

@ -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

View File

@ -10,6 +10,7 @@ let
({ lib, ... } : { config = { inherit (device) kernel; }; }) ({ lib, ... } : { config = { inherit (device) kernel; }; })
<liminix-config> <liminix-config>
./modules/s6 ./modules/s6
./modules/users.nix
] nixpkgs.pkgs; ] nixpkgs.pkgs;
squashfs = liminix.builders.squashfs config.filesystem.contents; squashfs = liminix.builders.squashfs config.filesystem.contents;
kernel = callPackage ./kernel { kernel = callPackage ./kernel {

View File

@ -33,10 +33,27 @@ in {
type = types.attrsOf types.nonEmptyStr; type = types.attrsOf types.nonEmptyStr;
}; };
}; };
groups = mkOption {
type = types.attrsOf types.anything;
};
users = mkOption {
type = types.attrsOf types.anything;
};
}; };
config = { config = {
defaultProfile.packages = with pkgs; defaultProfile.packages = with pkgs;
[ s6-init-bin busybox execline s6-linux-init s6-rc ]; [ s6-init-bin busybox execline s6-linux-init s6-rc ];
users.root = {
uid = 0; gid= 0; gecos = "Root of all evaluation";
dir = "/";
passwd = "";
shell = "/bin/sh";
};
groups.root = {
gid = 0; usernames = ["root"];
};
filesystem = dir { filesystem = dir {
bin = dir { bin = dir {
sh = symlink "${busybox}/bin/sh"; sh = symlink "${busybox}/bin/sh";
@ -57,8 +74,6 @@ in {
PATH=${lib.makeBinPath config.defaultProfile.packages} PATH=${lib.makeBinPath config.defaultProfile.packages}
export PATH export PATH
''); '');
passwd = { file = "root::0:0:root:/:/bin/sh\n"; };
group = { file = "root::0:\n"; };
}; };
proc = dir {}; proc = dir {};
run = dir {}; run = dir {};

26
modules/users.nix Normal file
View File

@ -0,0 +1,26 @@
{ lib, pkgs, config, ...}:
let
inherit (lib) concatStrings concatStringsSep mapAttrsToList; # mkEnableOption mkOption types isDerivation isType hasAttr ;
inherit (builtins) toString;
inherit (pkgs.pseudofile) dir symlink;
# inherit (pkgs) busybox;
passwd-file =
let lines = mapAttrsToList (name: u: "${name}:${if u ? passwd then u.passwd else "!!"}:${toString u.uid}:${toString u.gid}:${u.gecos}:${u.dir}:${u.shell}\n" )
config.users;
in concatStrings lines;
group-file =
let lines = mapAttrsToList
(name: {gid, usernames ? []}:
"${name}:x:${toString gid}:${concatStringsSep "," usernames}\n" )
config.groups;
in concatStrings lines;
in {
config = {
filesystem = dir {
etc = dir {
passwd = { file = passwd-file; };
group = { file = group-file; };
};
};
};
}

View File

@ -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 {

View File

@ -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;

View 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
'';
}

View File

@ -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 ] ;
} }

View File

@ -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\"))'"

View 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)