From b2fe8986a66a9a130e41b8b9087458ab3e80816a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Asbj=C3=B8rn=20Sloth=20T=C3=B8nnesen?= <ast@2e8.dk>
Date: Sat, 5 Aug 2023 12:15:49 +0000
Subject: initial commit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Asbjørn Sloth Tønnesen <ast@2e8.dk>
---
 README.md   |   7 ++
 qemu        | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test-in.xml |  29 ++++++++
 3 files changed, 259 insertions(+)
 create mode 100644 README.md
 create mode 100755 qemu
 create mode 100644 test-in.xml

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c017cc1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+- Install hook into /etc/libvirt/hooks/qemu
+- Restart libvirt
+- Profit
+
+## Testing
+
+Run `./qemu test`
diff --git a/qemu b/qemu
new file mode 100755
index 0000000..c1b0206
--- /dev/null
+++ b/qemu
@@ -0,0 +1,223 @@
+#!/bin/sh
+
+# routed subnet qemu hook for libvirt (L2 over L3)
+#
+# https://2e8.dk/libvirt-routed-subnet
+
+# Example XML:
+# <domain>
+#   <metadata>
+#     <routed-subnet xmlns="https://2e8.dk/libvirt-routed-subnet">
+#       <interface mac="xx:xx:xx:xx:xx:xx" mtu="9000">
+#         <gateway address="192.0.2.0/24"/>
+#         <host address="192.0.2.42"/>
+#       </interface>
+#     </routed-subnet>
+#   </metadata>
+# </domain>
+
+set -e
+
+routingxml=""
+fullxml=""
+device=""
+dry_run=0
+
+IP="$(which ip)"
+ip(){
+	if [ $dry_run -eq 0 ] ; then
+		"$IP" "$@"
+	else
+		echo ip "$@"
+	fi
+}
+
+SYSCTL="/usr/sbin/sysctl"
+sysctl(){
+	if [ $dry_run -eq 0 ] ; then
+		"$SYSCTL" "$@"
+	else
+		echo sysctl "$@"
+	fi
+}
+
+xpath(){
+	echo "$2" | xmllint --xpath "$1" - 2>/dev/null
+}
+
+get_address_family(){ [ -z "${1##*:*}" ] && echo ipv6 || echo ipv4; }
+
+get_config(){
+	local configfile="$1"
+	local ns="https://2e8.dk/libvirt-routed-subnet"
+	fullxml="$(cat "$configfile")"
+	routingxml="$(xpath "//*[local-name()='routed-subnet' and namespace-uri()='$ns']" "$fullxml")"
+}
+
+get_device(){
+	xpath "string(//interface[@type = 'ethernet']/mac[@address='$1']/../target/@dev)" "$fullxml"
+}
+
+get_element(){
+	xpath "//*[local-name()='$1'][$2]" "$3"
+}
+
+get_attr(){
+	xpath "string(*/@$1)" "$2"
+}
+
+node_loop(){
+	local handler="$1"
+	shift
+	local i=1
+	local ret=1
+	while [ $i -lt 1000 ]; do
+		$handler $i "$@" || return $ret
+		i=$(( $i + 1 ))
+		ret=0
+	done
+	return 2
+}
+
+process_gateway(){
+	local i=$1
+	local ifacexml="$2"
+	local nodexml="$(get_element 'gateway' "$i" "$ifacexml")"
+	if [ "$nodexml" = "" ] ; then
+		return 1
+	fi
+
+	if [ $dry_run -ne 0 ] ; then
+		echo "$nodexml"
+	fi
+
+	local address="$(get_attr 'address' "$nodexml")"
+
+	ip addr add "$address" dev "$device"
+
+	return 0
+}
+
+process_host(){
+	local i=$1
+	local ifacexml="$2"
+	local nodexml="$(get_element 'host' "$i" "$ifacexml")"
+	if [ "$nodexml" = "" ] ; then
+		return 1
+	fi
+
+	if [ $dry_run -ne 0 ] ; then
+		echo "$nodexml"
+	fi
+
+	local address="$(get_attr 'address' "$nodexml")"
+	local family="$(get_address_family "$address")"
+
+	if [ "$family" = "ipv4" ] ; then
+		ip route add proto static "$address/32" dev "$device"
+	fi
+	return 0
+}
+
+process_route(){
+	local i=$1
+	local ifacexml="$2"
+	local nodexml="$(get_element 'route' "$i" "$ifacexml")"
+	if [ "$nodexml" = "" ] ; then
+		return 1
+	fi
+
+	if [ $dry_run -ne 0 ] ; then
+		echo "$nodexml"
+	fi
+
+	local prefix="$(get_attr 'prefix' "$nodexml")"
+	local nexthop="$(get_attr 'nexthop' "$nodexml")"
+
+	ip route add proto static "$prefix" via "$nexthop"
+	return 0
+}
+
+process_iface(){
+	local i=$1
+	local ifacexml="$(get_element 'interface' "$1" "$routingxml")"
+	if [ "$ifacexml" = "" ] ; then
+		return 1
+	fi
+
+	local mac="$(xpath "string(/interface/@mac)" "$ifacexml")"
+	if [ "$mac" = "" ] ; then
+		return 1
+	fi
+
+	device="$(get_device "$mac")"
+	if [ "$device" = "" ] ; then
+		return 1
+	fi
+
+	local mtu="$(xpath "string(/interface/@mtu)" "$ifacexml")"
+	if [ "$mtu" != "" ] ; then
+		ip link set mtu "$mtu" dev "$device"
+	fi
+
+	prepare_device
+	node_loop process_gateway "$ifacexml" || true
+	node_loop process_host "$ifacexml" || true
+	node_loop process_route "$ifacexml" || true
+
+	return 0
+}
+
+prepare_device(){
+	# IPv4
+	sysctl -q -w "net.ipv4.conf.$device.proxy_arp=1"
+	sysctl -q -w "net.ipv4.conf.$device.rp_filter=1"
+	sysctl -q -w "net.ipv4.conf.$device.forwarding=1"
+
+	# IPv6
+	sysctl -q -w "net.ipv6.conf.$device.forwarding=1"
+	sysctl -q -w "net.ipv6.conf.$device.accept_ra=0"
+
+	# link up
+	ip link set up dev "$device"
+}
+
+start_network(){
+	node_loop process_iface || true
+}
+
+usage(){
+	echo "Usage: $0 <guest_name> start begin <xmlfile>"
+	echo "config: https://2e8.dk/libvirt-routed-subnet"
+	echo "libvirt docs: https://www.libvirt.org/hooks.html"
+}
+
+test_main(){
+	dry_run=1
+	main foobar start begin test-in.xml
+}
+
+main(){
+	if [ $# -eq 1 ] && [ "$1" = 'test' ] ; then
+		test_main
+		exit $?
+	elif [ $# -ne 4 ] ; then
+		echo 'too few arguments' >&2
+		usage >&2
+		exit 1
+	fi
+	local guest="$1"
+	local event="$2"
+	local evtype="$3"
+	local configfile="$4"
+	local state="${event}_${evtype}"
+	get_config "$configfile"
+
+	case "$state" in
+	start_begin)
+		start_network "$@"
+		;;
+	esac
+}
+
+main "$@"
diff --git a/test-in.xml b/test-in.xml
new file mode 100644
index 0000000..59af475
--- /dev/null
+++ b/test-in.xml
@@ -0,0 +1,29 @@
+<domain type='kvm'>
+  <name>testhost</name>
+  <metadata>
+    <routed-subnet xmlns="https://2e8.dk/libvirt-routed-subnet">
+      <interface mac="fa:12:34:56:78:90">
+        <gateway address="192.0.2.1/29"/>
+        <host address="192.0.2.2"/>
+	<route prefix="192.0.2.128/25" nexthop="192.0.2.2"/>
+	<gateway address="2001:db8:42::1/64"/>
+	<host address="2001:db8:42::2"/>
+	<route prefix="2001:db8:4242::/64" nexthop="2001:db8:42::2"/>
+      </interface>
+      <interface mac="fa:12:34:ab:cd:ef" mtu="9000">
+        <gateway address="192.0.2.9/29"/>
+        <host address="192.0.2.10"/>
+      </interface>
+    </routed-subnet>
+  </metadata>
+  <devices>
+    <interface type="ethernet">
+      <mac address="fa:12:34:56:78:90"/>
+      <target dev="foo0"/>
+    </interface>
+    <interface type="ethernet">
+      <mac address="fa:12:34:ab:cd:ef"/>
+      <target dev="bar0"/>
+    </interface>
+  </devices>
+</domain>
-- 
cgit v1.2.1