SwordArMor

Mise en place d’un NanoKVM sur mon routeur

J’ai récemment fait l’acquisition d’un NanoKVM Full afin de pouvoir reprendre la main sur mon routeur (qui est une machine ARM sans IPMI intégré) si jamais je foire sa configuration réseau alors que je ne suis pas chez moi. Je n’avais jusqu’alors jamais eu d’OOB sur mon routeur à la maison, bien que j’ai un backup 4G en cas de défaillance de ma FTTH (principalement dû à des écrasements/rebranchements). Ce qui fait que je suis peu ou prou protégé contre la bêtise de l’infra FTTH, mais pas contre ma propre bêtise.
Mon setup est fait de telle façon que le routeur 4G n’est pas là pour complètement prendre la main du routeur de la FTTH, mais pour servir de support à des tunnels wireguard qui ont un cost OSPF plus élevé que le PPP sur la FTTH. De cette façon, j’ai fallback transparent (modulo le MTU) entre les connexions. Par contre, si jamais le routeur FTTH tombe, la 4G tombe avec. Je prévoie d’avoir une redondance complète le jour où j’aurai une deuxième fibre, mais ça viendra dans un second temps.
Histoire de pouvoir prendre la main sur mon NanoKVM même en cas de coupure de fibre je veux avoir la main dessus par la 4G, mais aussi directement via mon LAN pour éviter de bouffer mon forfait 4G pour rien lorsque je l’utilise depuis chez moi (par exemple quand j’installe une nouvelle machine et que j’ai la flemme de brancher un écran dessus).

La configuration de base de mon routeur 4G est de nater tout ce que va vers l’interface 4G. Le plus propre serait d’avoir du routage dynamique, mais la flash n’est pas assez grosse pour installer ce daemon en plus ; après avoir installé snmpd et tcpdump dessus, il ne reste que 1.2M libres sur la flash. De plus, bird n’est pas packagé et frr n’est pas particulièrement léger.
J’ai donc dessus une route par défaut sur la 4G (qmimux0), mes IPs en more-spec sur l’interco avec mon routeur FTTH (br-lan), et un /30 dédié au NanoKVM (br-ifLan1). J’ai gardé la configuration en DHCP côté NanoKVM comme ça je peux facilement le brancher ailleurs si besoin, d’où le besoin de /30 (pour avoir l’IP de broadcast) au lieu d’un classique /31. Et avec ça, je peux accéder au NanoKVM depuis chez moi et en même temps faire monter le VPN wireguard par la 4G.

root@Teltonika-RUT950:~# ip r
default dev qmimux0 proto static scope link src 10.222.240.167 metric 1
10.0.0.0/8 via 10.0.4.1 dev br-lan proto static metric 3
10.0.4.0/31 dev br-lan proto static scope link metric 3
10.0.4.64/30 dev br-ifLan1 proto kernel scope link src 10.0.4.65
10.222.240.167 dev qmimux0 proto static scope link metric 1
45.91.126.0/24 via 10.0.4.1 dev br-lan proto static metric 3
172.16.0.0/12 via 10.0.4.1 dev br-lan proto static metric 3
192.168.0.0/16 via 10.0.4.1 dev br-lan proto static metric 3
root@Teltonika-RUT950:~# grep -C2 nanokvm /etc/config/dhcp
config host
	option mac '48:da:35:6f:a0:73'
	option name 'nanokvm'
	option ip '10.0.4.66'

alarig@x280 ~ % ping nanokvm.int.no.swordarmor.fr
PING nanokvm.int.no.swordarmor.fr (10.0.4.66) 56(84) bytes of data.
64 bytes from nanokvm.int.no.swordarmor.fr (10.0.4.66): icmp_seq=1 ttl=62 time=1.71 ms
64 bytes from nanokvm.int.no.swordarmor.fr (10.0.4.66): icmp_seq=2 ttl=62 time=1.59 ms
^C
--- nanokvm.int.no.swordarmor.fr ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 1.591/1.649/1.707/0.058 ms

Arrivé ici je vais naturellement sur l’interface web (le port ssh n’était pas ouvert par défaut) pour voir si je peux activer le SSH, et on peut !
Une fois loggué en root, je commence à regarder ce que j’ai sous la main, par simple curiosité, mais je pense que vous me comprendrez. Nous avons donc affaire à un Linux (pas surprenant) qui tourne sur un RISC-V 64-bit (malin pour avoir une faible conso) dont je connais pas le modèle exact parce que lscpu n’est pas dispo. Il s’agît d’une disto custom, /etc/os-release parle de Buildroot 2023.11.2, mais on a tout le userland habituel et même htop ainsi que 6.4G encore disponibles sur la partition principale.
Pour les curieux, voici le dmesg.

Puisque je suis root, et bien que l’interface web ne propose pas ces options, mon premier réflexe est d’apporter quelques finitions au système afin de l’intégrer au mieux au reste de mon infra. Cela commence donc par utiliser mon propre résolveur DNS et serveur NTP et aussi exporter les logs.
Pour les deux premiers c’est très facile, les fichiers de configuration habituels sont déjà là, il suffit donc de les éditer afin d’avoir les valeurs que je veux puis ensuite de redémarrer les services.
Par contre pour les logs, je n’en ai pas trouvé. Je suis donc allé voir comment le syslogd est lancé afin de savoir si je pouvais utiliser un fichier de configuration absent par défaut ou bien des variables d’environnement. Mon premier réflexe a été de lister les units systemd mais la commande n’existe pas.

root@kvm-a073 ~ # systemctl list-units
bash: systemctl: command not found

C’est somme toute assez logique puisque nous sommes tout de même sur une petite machine, et systemd est tout sauf léger. L’étape suivante est donc de fouiller /etc/init.d à l’ancienne, et bingo !

root@kvm-a073 ~ # find /etc/init.d/ -iname '*log*'
/etc/init.d/S02klogd
/etc/init.d/S01syslogd
root@kvm-a073 ~ # cat /etc/init.d/S01syslogd
#!/bin/sh

DAEMON="syslogd"
PIDFILE="/var/run/$DAEMON.pid"

SYSLOGD_ARGS=""

# shellcheck source=/dev/null
[ -r "/etc/default/$DAEMON" ] && . "/etc/default/$DAEMON"

# BusyBox' syslogd does not create a pidfile, so pass "-n" in the command line
# and use "-m" to instruct start-stop-daemon to create one.
start() {
	printf 'Starting %s: ' "$DAEMON"
	# shellcheck disable=SC2086 # we need the word splitting
	start-stop-daemon -b -m -S -q -p "$PIDFILE" -x "/sbin/$DAEMON" \
		-- -n $SYSLOGD_ARGS
	status=$?
	if [ "$status" -eq 0 ]; then
		echo "OK"
	else
		echo "FAIL"
	fi
	return "$status"
}

stop() {
	printf 'Stopping %s: ' "$DAEMON"
	start-stop-daemon -K -q -p "$PIDFILE"
	status=$?
	if [ "$status" -eq 0 ]; then
		rm -f "$PIDFILE"
		echo "OK"
	else
		echo "FAIL"
	fi
	return "$status"
}

restart() {
	stop
	sleep 1
	start
}

case "$1" in
	start|stop|restart)
		"$1";;
	reload)
		# Restart, since there is no true "reload" feature.
		restart;;
	*)
		echo "Usage: $0 {start|stop|restart|reload}"
		exit 1
esac

Le script d’init parle donc de la variable SYSLOGD_ARGS sourcée depuis le fichier /etc/default/syslogd. Un file nous apprend qu’il s’agît de l’implémentation busybox (/sbin/syslogd: symbolic link to ../bin/busybox. On peut ainsi aller lire le man de busybox pour savoir quelles options on peut utiliser pour avoir le droit au bonheur.

root@kvm-a073 ~ # ps aux | grep log
  224 root     /sbin/syslogd -n
  237 root     /sbin/klogd -n
 1671 root     grep log
root@kvm-a073 ~ # cat /etc/default/syslogd 
SYSLOGD_ARGS="-R msi.no.swordarmor.fr -L"
root@kvm-a073 ~ # /etc/init.d/S01syslogd restart
Stopping syslogd: OK
Starting syslogd: OK
root@kvm-a073 ~ # ps aux | grep log
  237 root     /sbin/klogd -n
 1935 root     /sbin/syslogd -n -R msi.no.swordarmor.fr -L
 1941 root     grep log

Tout cela est très sympathique mais ce que j’aimerais quand même à un moment c’est avoir un wireguard dessus pour prendre la main via la 4G, sinon on va encore dire que je me disperse. Malheureusement l’interface web ne parle que de tailscale mais pas d’un simple wireguard comme je l’aimerais. Qu’à cela ne tienne je suis root, je vais trouver un arrangement.
Le classique /etc/rc.local pour configurer le réseau sous debian lancer des commandes au boot n’est pas vide mais parle de fichiers qui n’existent pas. Dans le doute je me dis que ça risque d’être écrasé lors d’une mise à jour. Je ne peux pas non plus lancer un cron @reboot car le daemon est installé mais n’est pas lancé.

root@kvm-a073 ~ # cat /etc/rc.local
#!/bin/sh

echo "HELLO WORLD"

if [ -e /boot/maixcam ]
then
	/maixapp/apps/launcher/launcher &
fi
root@kvm-a073 ~ # file /boot/maixcam
/boot/maixcam: cannot open `/boot/maixcam' (No such file or directory)
root@kvm-a073 ~ # file /maixapp/apps/launcher/launcher
/maixapp/apps/launcher/launcher: cannot open `/maixapp/apps/launcher/launcher' (No such file or directory)
root@kvm-a073 ~ # which crond
/usr/sbin/crond
root@kvm-a073 ~ # ps aux | grep cron
 1384 root     grep cron

Donc il ne me reste plus qu’à faire les choses proprement : écrire un script d’init. Je me dis que c’est une bonne idée de se mettre au même niveau de priorité que sshd, puisqu’il a également besoin que le réseau soit up avant de démarrer. Je ne m’embête pas avec la gestion du PID vu que c’est un module kernel que je lance, ni avec les commandes pour faire tomber le VPN vu que j’ai pas prévu de le faire, mais ça serait assez trivial à ajouter.

root@kvm-a073 ~ # find /etc/init.d/ -iname '*ssh*'
/etc/init.d/S50sshd
root@kvm-a073 ~ # vim /etc/wireguard/wg0.conf
root@kvm-a073 ~ # sed 's/PrivateKey.*/PrivateKey = blah/' /etc/wireguard/wg0.conf
[Interface]
PrivateKey = blah

[Peer]
#Endpoint = ovpn.fr.swordarmor.fr:10017
Endpoint = 89.234.186.219:10017
PublicKey = ZzPmmpKns18Q6lpJ5V+T6MenBXjMEx2qDDj58yaIzTo=
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
root@kvm-a073 ~ # vim /etc/init.d/S50wireguard
root@kvm-a073 ~ # cat /etc/init.d/S50wireguard
#!/bin/sh
case "$1" in
	start)
		echo "Start ovpn.fr connection"
		ip link add dev wg0 type wireguard
		wg setconf wg0 /etc/wireguard/wg0.conf
		ip addr add 10.0.1.9/31 dev wg0
		ip link set up dev wg0
		ping -c1 10.0.1.8
		#ip route add 0.0.0.0/0 dev wg0
		;;
	stop)
		;;
	restart|reload)
		"$0" stop
		"$0" start
		;;
	*)
		echo "Usage: $0 {start|stop|restart}"
	exit 1
esac
exit $?
root@kvm-a073 ~ # chmod +x /etc/init.d/S50wireguard
root@kvm-a073 ~ # /etc/init.d/S50wireguard start
root@kvm-a073 ~ # ip a
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0:  mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 48:da:35:6f:a0:73 brd ff:ff:ff:ff:ff:ff permaddr c2:e8:e7:a3:16:80
    inet 10.0.4.66/30 brd 10.0.4.67 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::4ada:35ff:fe6f:a073/64 scope link
       valid_lft forever preferred_lft forever
3: ip6tnl0@NONE:  mtu 1452 qdisc noop state DOWN group default qlen 1000
    link/tunnel6 :: brd :: permaddr 8aa8:c467:33d4::
4: usb0:  mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 48:da:35:6e:a0:73 brd ff:ff:ff:ff:ff:ff
    inet 10.160.115.1/24 scope global usb0
       valid_lft forever preferred_lft forever
    inet6 fe80::4ada:35ff:fe6e:a073/64 scope link
       valid_lft forever preferred_lft forever
5: wg0:  mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.0.1.9/31 scope global wg0
       valid_lft forever preferred_lft forever

Donc on a bien un tunnel wireguard en plus du LAN, avec la MTU par défaut qui semble être fonctionnelle désormais ; même si lorsque j’avais monté les tunnels pour redonder ma FTTH avec la 4G j’avais dû abaisser le MTU à 1340.

root@kvm-a073 ~ # tracepath ovpn.fr.swordarmor.fr
 1?: [LOCALHOST]                      pmtu 1500
 1:  Teltonika-RUT950.com                                  2.881ms 
 1:  eth0.105.rut950.int.no.swordarmor.fr                  2.950ms 
 2:  no reply
 3:  192.168.8.62                                         59.745ms asymm  2 
 4:  192.168.255.22                                       47.473ms asymm  7 
 5:  no reply
 6:  no reply
 7:  no reply
 8:  no reply
 9:  no reply
10:  no reply
11:  no reply
12:  no reply
13:  no reply
14:  no reply
15:  no reply
16:  ovpn.fr.as208627.net                                 87.731ms reached
     Resume: pmtu 1500 hops 16 back 13 
La configuration de mon routeur wireguard est de nater tout ce qui va vers un tunnel. L’utilité première est de prendre la main sur des machines nomades à distance pour lesquelles je dois assurer la maintenance (celles de la famille quoi) et sur lesquelles je ne maîtrise pas forcément le routage de leur LAN.
Ici ça m’arrange parce que du coup j’ai pas besoin de gérer des tables de routage différentes sur le NanoKVM entre le LAN et le wireguard. L’IP source vue par le KVM sur le wireguard sera forcément celle sur la route directement connectée et le LAN pourra continuer à utiliser la default, donc la réponse ira au bon endroit.
Le mtr montre bien que je fais le tour de mon réseau pour revenir chez moi. Je ne distribue pas d’IP publique dans le VPN parce que je ne vois pas spécialement de cas d’usage où je ne pourrais pas monter mon propre VPN pour y accéder.

alarig@x280 ~ % mtr -bzwe nanokvm-wg.int.no.swordarmor.fr
Start: 2026-03-07T15:44:08+0100
HOST: x280.int.no.swordarmor.fr                                              Loss%   Snt   Last   Avg  Best  Wrst StDev
  1. AS???         enp3s0.101.core02-rennes.fr.as208627.net (192.168.5.254)   0.0%    10    1.1   1.1   0.8   1.2   0.1
  2. AS???         ppp0.edge01-th2lf.swordarmor.fr (10.0.4.16)                0.0%    10    6.9   7.0   6.3   8.4   0.7
  3. AS???         eno1.104.regis.swordarmor.fr (10.0.4.24)                   0.0%    10   13.6  13.9  13.1  15.7   0.8
  4. AS204092      ovpn.fr.as208627.net (89.234.186.219)                      0.0%    10   14.1  13.9  13.1  15.2   0.6
  5. AS???         nanokvm-wg.int.no.swordarmor.fr (10.0.1.9)                 0.0%    10   57.4  79.0  53.0 119.6  19.3

Et maintenant qu’on a tout ça, on peut enfin regarder si la fonction de KVM est bien remplie. Donc déjà premier test : est-ce qu’il reste bien allumé si je redémarre le routeur ? (si jamais il prenait l’alimentation électrique depuis l’USB qui sert à envoyer les I/O clavier).
Et c’est bien le cas, l’uptime du KVM est supérieur à celui du routeur.

alarig@x280 ~ % ssh root@nanokvm.int.no.swordarmor.fr
root@nanokvm.int.no.swordarmor.fr's password:
# uptime
 11:57:12 up 23 min,  load average: 5.30, 5.57, 4.10
#

alarig@x280 ~ % ssh root@core01-rennes.fr.swordarmor.fr
Linux core01-rennes.fr.swordarmor.fr 6.18.12-gentoo-core01-arm #1 SMP PREEMPT_DYNAMIC Fri Feb 20 00:23:04 CET 2026 aarch64 GNU/Linux
Last login: Sat Mar  7 11:28:15 CET 2026 from 192.168.5.1 on pts/1
core01-rennes ~ # uptime
 12:57:32 up 8 min,  1 user,  load average: 0.17, 0.12, 0.07
core01-rennes ~ #

J’ai bien les logs dmesg qui me disent que l’émulation de clavier/souris est présente. J’ai même le /data du NanoKVM qui est exposé.

core01-rennes ~ # dmesg | grep -i NanoKVM
[    3.190596] usb 16-1: Product: NanoKVM
[    3.274675] input: sipeed NanoKVM as /devices/platform/PNP0D10:09/usb16/16-1/16-1:1.2/0003:3346:1009.0001/input/input1
[    3.426555] hid-generic 0003:3346:1009.0001: input,hidraw0: USB HID v1.01 Keyboard [sipeed NanoKVM] on usb-PNP0D10:09-1/input2
[    3.427383] input: sipeed NanoKVM as /devices/platform/PNP0D10:09/usb16/16-1/16-1:1.3/0003:3346:1009.0002/input/input2
[    3.427588] hid-generic 0003:3346:1009.0002: input,hidraw1: USB HID v1.01 Mouse [sipeed NanoKVM] on usb-PNP0D10:09-1/input3
[    3.428332] input: sipeed NanoKVM as /devices/platform/PNP0D10:09/usb16/16-1/16-1:1.4/0003:3346:1009.0003/input/input3
[    3.428452] hid-generic 0003:3346:1009.0003: input,hidraw2: USB HID v1.01 Mouse [sipeed NanoKVM] on usb-PNP0D10:09-1/input4
[    4.537290] scsi 0:0:0:0: Direct-Access     NanoKVM  USB Mass Storage 0520 PQ: 0 ANSI: 2
core01-rennes ~ # fdisk -l /dev/sda
Disk /dev/sda: 21.49 GiB, 23076012032 bytes, 45070336 sectors
Disk model: USB Mass Storage
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x00000000

Et quand je vais sur l’interface web, j’ai bien la sortie tty et je peux taper des trucs dedans sans avoir des caractères bizarres comme sur les IPMI supermicro quand il y a un mix qwerty/azery.
Capture d’écran de la sortie KVM dans firefox

La puissance du CPU ne semble pas être très élevée, puisque quand j’ai téléchargé l’ISO d’arch (qui fait 1.4Go) ça a pris un peu plus de vingt minutes, soit un débit de ~10Mbps. Pendant ce temps le CPU était à 100% tout du long. Mais c’est largement suffisant pour afficher la sortie console et je ne compte pas passer mon temps à télécharger plein d’ISO différents.
Donc à voir sur la durée si mes bricoles résistent aux upgrades, mais ça me semble être un produit de qualité tout à fait respectable et largement hackable.

Essai d’un routeur ARM

Dans la mesure où ARM prend de plus en plus d’ampleur, ça faisait quelques temps que je voulais voir ce que ça donnait de router avec, notamment par rapport à la gestion des IRQ et de la fréquence.
En effet, pour router il n’y a pas besoin d …

Lire la suite

Installer des optiques 10G ZR directement dans une carte réseau PCI

Dans un contexte de FAI associatif comme grifon on cherche à vraiment limiter les coûts. Cela nous amène à des architectures où des machines peuvent faire à la fois routeur et serveur. C’est le cas de la machine que nous avons à TH2 : grifon la partage avec stolon afin …

Lire la suite

Sécurisation du routage BGP en utilisant ASPA avec routinator et bird (cas de Breizh-IX)

Le BGP est un protocole de routage dynamique qui se base historiquement sur la confiance. En théorie, n’importe qui peut plus ou moins annoncer n’importe quoi, et ça sera pris en compte. En pratique, les peerings BGP sont protégés par des prefix-lists afin de ne propager que ce …

Lire la suite

mautrix-signal ⚠ Your message may not have been bridged: 110: Verification failure in zkgroup

Pour éviter d’avoir plein d’applications de messagerie instantanée sur mon téléphone, je bridge tout dans matrix et je n’utilise directement que matrix. Ma machine matrix était sur une infra d’hypervision que je n’ai plus vraiment envie d’utiliser, et j’ai depuis une plus grosse …

Lire la suite

Le concept d’empty non terminal (ENT) en DNS, et son interaction avec les wildcards

Cet article est écrit en collaboration avec Axel Viala. Tout au long de l’article nous parlons de serveurs faisant autorité, les révolveurs n’ayant pas ce cas à gérer.

Sur un nom de domaine, on peut se retrouver avec des sous-domaines vides mais ayant des enfants. Nous appelons cela …

Lire la suite

Faire tenir la charge à une instance mastodon malgré Musk

Si vous lisez ce blog, vous savez sûrement qu’un riche mégalomane étasunien a racheté twitter, et qu’on en attendait pas moins de son mode de gestion. Cela a entraîné un exode vers mastodon, qui ronronnait tranquillement dans son coin jusqu’ici.
Je vais ici partager les différentes actions …

Lire la suite

Avoir une sorte de flowspec sous linux avec ipset

Internet étant truffé de machines plus ou moins vérolées qui scannent le monde entier, j’ai eu envie de les bloquer à l’échelle complète de mon réseau ; un genre de fail2ban2bgp.
Avec netfilter il n’existe pas de mécanisme pour avoir des règles dynamiques, et donc pas de moyen …

Lire la suite

Remonter le constructeur et le modèle d’un serveur en SNMP

Par défaut, la configuration de net-snmpd ne remonte pas le constructeur et le modèle d’un serveur, ce qui fait que LibreNMS (ou Observium) vont afficher « Generic x86 64-bit » là où on pourrait avoir « Dell Inc. [PowerEdge R510] » ou « HP [ProLiant DL320e Gen8] », ce qui rend plus facile l’inventorisation …

Lire la suite

Installer son instance mastodon sous Gentoo

Suite à l’annonce de Valère d’arrêter les services hostux, et donc l’instance mastodon, j’ai décidé de monter la mienne. La principale raison est que je n’ai pas trouvé d’autre instance en Europe avec un TLSA. Je ne vais pas ici expliquer toute l’installation …

Lire la suite