Fibre Orange : Remplacer sa Livebox par un routeur CentOS Stream 8

Je suis abonné à l’offre Fibre d’Orange depuis 2016. Et si je suis globalement satisfait de la qualité du réseau, je ne peux pas en dire autant de la Livebox fournie par Orange. Les limitations sont nombreuses pour un geek souhaitant faire de l’hébergement de services à la maison : une seule plage IPv6 en /64, pas de configuration possible des tables de routage pour avoir plusieurs sous-réseaux IPv4, etc. J’ai donc décidé de remplacer la Livebox par un routeur basé sur CentOS Stream 8. Et l’aventure ne fut pas un long fleuve tranquille ! Cet article présente la configuration que j’ai mise en place et qui me donne aujourd’hui satisfaction.

Besoin

En tant que geek, j’ai des besoins bien particuliers quant au routeur qui gère mon accès internet :

J’ai volontairement laissé de coté :

Matériel

Pour le serveur en lui-même, j’ai choisi un HP DL20 Gen9 pour les raisons suivantes :

Je l’ai déniché pour 660 € sur eBay avec les caractéristiques suivantes :

Serveur HP DL20 Gen9, vue de face, vue de dos.
Serveur HP DL20 Gen9, vue de face, vue de dos.

Pour me raccorder au réseau fibre optique d’Orange, j’ai conservé l’ONT (Optical Network Termination) qui m’a été fourni avec la Livebox. D’un coté, je branche la jarretière optique, et de l’autre je branche le câble RJ-45 qui va jusqu’au serveur HP.

Boitier Fibre Orange. Source: [Wikipedia](https://commons.wikimedia.org/wiki/File:Boitier-Fibre-Orange_-_IMG_6456.jpg)
Boitier Fibre Orange. Source: Wikipedia

Logiciel

Au moment où j’ai commencé à installer le serveur, la dernière version publiée de CentOS Stream était la 8. Mais partez plutôt sur la dernière version disponible !

L’installation de CentOS Stream s’effectue, comme la majorité des distributions Linux :

Spécificités de l’offre Fibre Orange

L’offre Fibre d’Orange a quelques spécificités, désormais bien connues des geeks de lafibre.info :

Cet article couvre l’ensemble de ces points.

Il est à noter qu’à partir du noyau 5.7, le filtre “egress” de netfilter devrait permettre la capture des paquets DHCPv4 et l’étiquetage de leur priorité. Le client DHCP patché ne serait alors plus nécessaire.

Commit e687ad60af09 (“netfilter: add netfilter ingress hook after handle_ing() under unique static key”) introduced the ability to classify packets on ingress.

Allow the same on egress.

This hook is also useful for NAT46/NAT64, tunneling and filtering of locally generated af_packet traffic such as dhclient.

Source: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=8537f78647c072bdb1a5dbe32e1c7e5b13ff1258

Installation du client DHCP patché

J’ai backporté le patch de Zoc dans les RPMs dhclient de CentOS Stream 8. Les sources sont dans le dépot Git nmasse-itix/dhclient-orange et les RPMs sont disponibles publiquement sur un partage Backblaze B2.

Installer le client DHCP patché.

sudo curl -o /etc/yum.repos.d/dhclient-orange.repo https://f003.backblazeb2.com/file/dhclient-orange/dhclient-orange.repo
sudo dnf remove dhcp-client
sudo dnf install dhcp-client-orange-isp

Configurer NetworkManager pour qu’il utilise dhclient plutôt que son client DHCP interne.

Fichier "/etc/NetworkManager/conf.d/dhclient.conf"
[main]
dhcp=dhclient

Redémarrer NetworkManager.

sudo systemctl restart NetworkManager

Configuration du client DHCP pour l’authentification Orange

Pour configurer le client DHCP pour l’authentification Orange, il vous faudra trois choses :

Saisir le contenu du fichier /etc/dhcp/dhclient.conf.

Fichier "/etc/dhcp/dhclient.conf"
option rfc3118-authentication code 90 = string;
option dhcp-client-identifier code 61 = string;

interface "eno2.832" {
    send vendor-class-identifier "sagem";
    send user-class "+FSVDSL_livebox.Internet.softathome.Livebox4";
    send dhcp-client-identifier 01:AA:BB:CC:DD:EE:FF;
    send rfc3118-authentication 00:00:00:00:00:00:00:00:00:00:00:1a:09:00:00:05:58:01:03:41:01:0B:66:74:69:2F:64:75:6D:6D:79:3c:12:31:32:33:34:35:36:37:38:39:30:31:32:33:34:35:36:03:13:41:b9:80:f2:ea:3f:06:3b:2b:e7:08:ac:ec:9c:38:9e:ba;
    request subnet-mask,routers,domain-name,broadcast-address,dhcp-lease-time,dhcp-renewal-time,dhcp-rebinding-time,rfc3118-authentication;
}

Configuration de l’interface réseau pour utiliser le VLAN 832

Configurer l’interface réseau (ici eno2) à l’aide de NetworkManager pour utiliser le VLAN 832.

sudo nmcli con add type ethernet con-name eno2.832 autoconnect yes ipv4.method disabled ipv6.method ignore connection.interface-name eno2
sudo nmcli con add type vlan dev eno2 con-name eno2.832 id 832 egress "0:0,1:0,2:0,3:0,4:0,5:0,6:6,7:0" autoconnect yes ipv4.method auto ipv6.method ignore

À ce stade, vous devez obtenir une adresse IPv4 publique lorsque vous activez l’interface.

sudo nmcli con up eno2.832

La suite de l’article est très dépendante de choix que j’ai pu faire par ailleurs dans mon réseau domestique. Aussi, considérez la comme une indication plus qu’un tutoriel pas à pas.

Configuration nécessaire à IPv6

Activer le routage des paquets IPv4 et IPv6. Ne pas oublier d’adapter le nom de l’interface réseau !

Fichier "/etc/sysctl.d/99-fibre-orange.conf"
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net/ipv6/conf/eno2.832/accept_ra=2
net.ipv4.conf.all.src_valid_mark=1

Recharger les paramètres noyaux avec la commande sysctl.

sudo sysctl --system

Configurer ensuite le client DHCPv6 pour l’authentification Orange. Même mode opératoire que pour IPv4, il vous faudra trois choses :

Saisir le contenu du fichier /etc/dhcp/dhclient6.conf.

Fichier "/etc/dhcp/dhclient.conf"
option dhcp6.auth code 11 = string;
option dhcp6.vendorclass code 16 = string;
option dhcp6.userclass code 15 = string;

interface "eno2.832" {
    send dhcp6.vendorclass 00:00:04:0e:00:05:73:61:67:65:6d;
    send dhcp6.userclass 00:2b:46:53:56:44:53:4c:5f:6c:69:76:65:62:6f:78:2e:49:6e:74:65:72:6e:65:74:2e:73:6f:66:74:61:74:68:6f:6d:65:2e:6c:69:76:65:62:6f:78:34;
    send dhcp6.vendor-opts 00:00:05:58:00:06:00:0e:49:50:56:36:5f:52:45:51:55:45:53:54:45:44;
    send dhcp6.client-id 00:03:00:01:AA:BB:CC:DD:EE:FF;
    send dhcp6.auth 00:00:00:00:00:00:00:00:00:00:00:1a:09:00:00:05:58:01:03:41:01:0B:66:74:69:2F:64:75:6D:6D:79:3c:12:31:32:33:34:35:36:37:38:39:30:31:32:33:34:35:36:03:13:41:b9:80:f2:ea:3f:06:3b:2b:e7:08:ac:ec:9c:38:9e:ba;
    also request dhcp6.name-servers, dhcp6.vendorclass, dhcp6.userclass, dhcp6.auth;
}

Créer le script de dispatch pour NetworkManager. Ce script sera appelé automatiquement par NetworkManager après un up ou un down de l’interface réseau et lancera le client DHCPv6 en mode Prefix Delegation. Ne pas oublier d’adapter le nom de l’interface réseau !

Fichier "/etc/NetworkManager/dispatcher.d/99-orange-ipv6"
#!/bin/bash

set -Eeuo pipefail

if [ "${DEVICE_IFACE:-}" != "eno2.832" ]; then
    exit 0
fi

# Set log file for this shell and all commands executed
exec &>>/var/log/orange-ipv6.log
trap 'ret=$? ; if [ $ret -gt 0 ]; then echo "NM dispatcher script called with action = ${NM_DISPATCHER_ACTION:-} finished at $(date -Isecond) with code $ret"; fi' EXIT ERR

case "$NM_DISPATCHER_ACTION" in
up)
    signal=TERM
    while [ -f "/var/run/NetworkManager/dhclient6-$DEVICE_IFACE.pid" ] && pgrep -F "/var/run/NetworkManager/dhclient6-$DEVICE_IFACE.pid" &>/dev/null; do
        if pkill -F /var/run/NetworkManager/dhclient6-$DEVICE_IFACE.pid --signal "$signal"; then
            signal=KILL
            sleep 5
        fi
    done
    dhclient -P -6 -cf /etc/dhcp/dhclient6.conf -lf "/var/lib/NetworkManager/dhclient-$CONNECTION_UUID-$DEVICE_IFACE.lease" -pf "/var/run/NetworkManager/dhclient6-$DEVICE_IFACE.pid" "$DEVICE_IFACE"
    ;;
down)
    if [ -f "/var/run/NetworkManager/dhclient6-$DEVICE_IFACE.pid" ]; then
        pkill -F /var/run/NetworkManager/dhclient6-$DEVICE_IFACE.pid || true
    fi
    ;;
*)
    ;;
esac

exit 0

Créer le répertoire /etc/dhcp/dhclient-exit-hooks.d.

sudo mkdir -p /etc/dhcp/dhclient-exit-hooks.d

Créer le script de hook pour dhclient. Il sera appelé par le client DHCPv6 après obtention d’un préfixe IPv6 et a pour tâche d’affecter les adresses IPv6 aux différentes interfaces du serveur. Ne pas oublier d’adapter le nom de l’interface réseau et le nom de vos interfaces réseaux internes (chez moi, elles s’appelle ivs1, ivs2, etc.) !

Fichier "/etc/dhcp/dhclient-exit-hooks.d/99-orange-ipv6"
#!/bin/bash

set -Eeuo pipefail

if [ "${interface:-}" != "eno2.832" ]; then
    exit 0
fi

# Issue a log in case of error
trap 'ret=$? ; if [ "$ret" -gt 0 ]; then echo "dhclient hook script called with reason = ${reason:-} finished at $(date -Isecond) with code $ret"; fi' EXIT ERR

ifprefix="ivs"
internal_ifaces="$(ifconfig -a -s | egrep -o "^($ifprefix)[0-9]+" | sort -V)"
external_iface="$interface"

temp="$(mktemp -d -t orange.XXXXXX)"
trap 'rm -rf "$temp"' EXIT

function log () {
    if [ -n "${DEBUG:-}" ]; then
        echo "$@"
    fi
    "$@"
}

function cleanup_interface () {
    local interface="$1"
    current_ipv6_addr="$(ip -6 -br addr show dev "$interface" scope global | awk 'NR == 1 { print $3 }')"
    if [ -n "$current_ipv6_addr" ]; then
        log ip addr delete "$current_ipv6_addr" dev "$interface" || true
    fi
}

case "${reason:-}" in
BOUND6|REBIND6)
    # This should be set on startup. To be safe, recheck here.
    sysctl -q net/ipv6/conf/eno2.832/accept_ra=2

    if [ -n "${new_ip6_prefix:-}" ] ; then
        ipcalc -S 64 "$new_ip6_prefix" --no-decorate > "$temp/networks"
        for interface in $internal_ifaces $external_iface; do
            if [ "$interface" == "$external_iface" ]; then
                # eno2.832 gets subnet 0
                ipv6_network="$(head -n 1 $temp/networks)"
            else
                if [[ "$interface" =~ ^ivs ]]; then
                    # ivsX gets subnet X (X is guaranted to start at 1)
                    n="${interface#ivs}"
                else
                    echo "Unsupported interface: $interface"
                    continue
                fi
                ipv6_network="$(awk -v "net_id=$n" 'NR == net_id + 1 { print }' $temp/networks)"
            fi

            # Determine current and previous IPv6 addresses
            new_ipv6_addr="$(echo "$ipv6_network" | sed -r 's|::/([0-9]+)|::1/\1|')"
            old_ipv6_addr="$(ip -6 -br addr show dev "$interface" scope global | awk 'NR == 1 { print $3 }')"

            # Only replace the previous IPv6 address if the new one is different
            if [ "${new_ipv6_addr}" != "$old_ipv6_addr" ]; then
                cleanup_interface "$interface"
                log ip addr add "$new_ipv6_addr" dev "$interface"
            fi
        done
    fi
    ;;
RELEASE)
    for interface in $internal_ifaces; do
        cleanup_interface "$interface"
    done
    ;;
*)
    ;;
esac

exit 0

N’oubliez pas de rendre ces deux scripts exécutables.

sudo chmod 755 /etc/NetworkManager/dispatcher.d/99-orange-ipv6
sudo chmod 755 /etc/dhcp/dhclient-exit-hooks.d/99-orange-ipv6

Pour que l’IPv6 fonctionne, il faut correctement étiqueter les paquets DHCPv6 en priorité 6. Pour cela, j’utilise nftables. Le script est très dépendant de mes choix de réseau, je vous invite donc à le prendre comme un exemple pour créer le votre ensuite.

Désactiver firewalld.

sudo systemctl stop firewalld
sudo systemctl disable firewalld
sudo systemctl mask firewalld

Éditer le contenu du fichier /etc/sysconfig/nftables.conf.

Fichier "/etc/sysconfig/nftables.conf"
# Uncomment the include statement here to load the default config sample
# in /etc/nftables for nftables service.

include "/etc/nftables/itix.nft"

# To customize, either edit the samples in /etc/nftables, append further
# commands to the end of this file or overwrite it after first service
# start by calling: 'nft list ruleset >/etc/sysconfig/nftables.conf'.

Créer le fichier /etc/nftables/update.nft.

Fichier "/etc/nftables/update.nft"
#!/usr/sbin/nft -f

flush table inet itix-fw
delete table inet itix-fw
flush table ip itix-nat
delete table ip itix-nat

include "/etc/nftables/itix.nft"

Créer le fichier /etc/nftables/itix.nft.

Fichier "/etc/nftables/itix.nft"
#!/usr/sbin/nft -f

table inet itix-fw {
    chain Public-Services {
        # Allow Ping
        icmp type echo-request counter accept

        # Allow SSH
        tcp dport { 22 } counter accept
    }

    chain Forward-IPv6-from-Internet {
        # Allow IPv6 ICMP
        ip6 nexthdr ipv6-icmp counter accept

        # Enable TCP/UDP ports > 1024
        tcp dport > 1024 counter accept
        udp dport > 1024 counter accept
    }

    chain Orange-IPv6-Priority {
        # DSCP is "Differenciated Service Code Point". See RFC 4594.
        # CS6 is "Class Selector 6 (Internetwork Control)".
        icmpv6 type { nd-neighbor-solicit, nd-router-solicit } ip6 dscp set cs6 meta priority set 0:6 counter
        udp sport { dhcpv6-client, dhcpv6-server } ip6 dscp set cs6 meta priority set 0:6 counter
    }

    chain Input {
        type filter hook input priority filter + 20
        policy drop

        # Accept packets related to existing connections
        ct state invalid counter drop
        ct state { established, related } counter accept

        # Loopback
        iifname lo counter accept

        # Accept all ethernet frames on the public interface so that we can then handle the VLANs
        iifname eno2 accept
        # Filter packets arriving on VLAN 832
        iifname eno2.832 counter jump Public-Services
        
        # Internal Interfaces
        iifname { ivs1, ivs2, ivs3, ivs4, ivs5 } counter accept
    }

    chain Output {
        type filter hook output priority filter + 20
        policy accept

        # Accept packets related to existing connections
        ct state invalid counter drop
        ct state { established, related } counter accept

        # Tag all DHCPv6 packets with priority 6
        oifname eno2.832 counter jump Orange-IPv6-Priority
    }

    chain Forward {
        type filter hook forward priority filter + 20
        policy drop
        
        # Accept packets related to existing connections
        ct state invalid counter drop
        ct state { established, related } counter accept

        # Loopback
        iifname lo counter accept

        # From the internal network to the internet
        iifname { ivs1, ivs2, ivs3, ivs4, ivs5 } oifname eno2.832 counter accept
        # From the internal network to the internal network
        iifname { ivs1, ivs2, ivs3, ivs4, ivs5 } oifname { ivs1, ivs2, ivs3, ivs4, ivs5 } counter accept
        # From the internet to the internal network
        iifname eno2.832 oifname { ivs1, ivs2, ivs3, ivs4, ivs5 } counter jump Forward-IPv6-from-Internet
    }
}

table ip itix-nat {
    chain Post-Routing {
        type nat hook postrouting priority srcnat
        policy accept

        # Masquerade all connections to the Internet
        iifname { ivs1, ivs2, ivs3, ivs4, ivs5 } oifname eno2.832 counter masquerade
    }

}

Activer et démarrer le service nftables.

sudo systemctl enable nftables
sudo systemctl start nftables

Vérifier avec la commande sudo nft list tables que les deux tables itix-fw et itix-nat ont bien été chargées.

A ce moment là, vous pouvez essayer de faire un nmcli con down eno2.832 puis nmcli con up eno2.832 et vérifier que vous avez bien une adresse IPv4 publique et une adresse IPv6 globale.

Vérification périodique et cohérence IPv4/IPv6

Orange applique des règles de cohérence entre les protocoles IPv4 et IPv6. Rien n’est documenté officiellement, mais sur le forum lafibre.info quelques indications ont été données.

Pour mettre en oeuvre ces règles, j’ai développé un script qui vérifie que les piles IPv4 et IPv6 sont opérationnelles et force un nouveau cycle DHCPv4 + DHCPv6 si nécessaire. Pour éviter tout problème, je force également un renouvellement des baux DHCP toutes les 12 heures.

Fichier "/usr/local/bin/fibre-orange"
#!/bin/bash

set -Eeuo pipefail

ORANGE_IFACE="eno2.832"

declare -a TARGET_IPV4=("8.8.8.8" "8.8.4.4" "1.1.1.1" "1.0.0.1" "208.67.222.222" "208.67.220.220")
declare -a TARGET_IPV6=("2001:4860:4860::8888" "2001:4860:4860::8844" "2606:4700:4700::1111" "2606:4700:4700::1001" "2620:119:53::53" "2620:119:35::35")

function help () {
    echo "Usage: $0 {help|health-check|renew}"
}

function error () {
    echo "$1" >&2
}

function msg () {
    echo "$1"
}

function die () {
    error "$1" "$@"
    exit 1
}

function ping () {
    /bin/ping "$1" -c 4 -I "$ORANGE_IFACE" -n -q -W 10 "$2" > /dev/null 2>&1
}

function orange_healthcheck () {
    declare ipv4_check=0
    for ipv4 in ${TARGET_IPV4[@]}; do
        if ping -4 "$ipv4"; then
            ipv4_check=1
        else
            msg "$ipv4 is not reachable!"
        fi
    done

    if [ "$ipv4_check" == "0" ]; then
        error "IPv4 stack is down!"
    fi

    declare ipv6_check=0
    for ipv6 in ${TARGET_IPV6[@]}; do
        if ping -6 "$ipv6"; then
            ipv6_check=1
        else
            msg "$ipv6 is not reachable!"
        fi
    done

    if [ "$ipv6_check" == "0" ]; then
        error "IPv6 stack is down!"
    fi

    if [[ "$ipv4_check" == "0" || "$ipv6_check" == "0" ]]; then
        return 1
    fi

    return 0
}

function kill_process () {
    declare signal=TERM
    while :; do
        if [ -f "/var/run/NetworkManager/$1-$ORANGE_IFACE.pid" ] && pkill -F /var/run/NetworkManager/$1-$ORANGE_IFACE.pid --signal "$signal"; then
            msg "Killed $1 with $signal!"
            signal=KILL
            sleep 5
        else
            break
        fi
    done

    return 0
}

function is_dhclient4_running () {
    if [ ! -f /var/run/NetworkManager/dhclient-$ORANGE_IFACE.pid ] || ! pgrep -F /var/run/NetworkManager/dhclient-$ORANGE_IFACE.pid &>/dev/null; then
        return 1
    fi
    return 0
}

function orange_renew () {
    # Stop all dhclient instances
    kill_process dhclient6
    kill_process dhclient

    # Sometimes the "nmcli device reapply" fails to restart the DHCP client.
    # Monitor the dhclient process to know when it succeeded.
    while ! is_dhclient4_running; do
        nmcli device reapply "$ORANGE_IFACE"
        sleep 30
    done

    return 0
}

case "${1:-}" in
health-check)
    if ! orange_healthcheck; then
        error "Renewing IPv4 and IPv6 DHCP leases..."
        nmcli connection down $ORANGE_IFACE
        sleep 2
        nmcli connection up $ORANGE_IFACE
    fi
    ;;
renew)
    orange_renew
    ;;
help)
    help
    ;;
*)
    error "Unkown action '${1:-}'!"
    help
    exit 1
    ;;
esac

exit 0

Rendre le script exécutable.

sudo chmod 755 /usr/local/bin/fibre-orange

Créer la crontab associée pour lancer ce script périodiquement.

Fichier "/etc/cron.d/fibre-orange"
*/5 * * * * root /usr/local/bin/fibre-orange health-check
0 */12 * * * root /usr/local/bin/fibre-orange renew

Conclusion

Cela fait déjà plusieurs années que le serveur est installé et avec les dernières informations glanées sur le forum lafibre.info, la configuration est robuste. Je n’ai pas tout expliqué dans l’article car la configuration que j’ai mise en place est complexe et n’apporterait pas grand chose pour le geek souhaitant remplacer sa Livebox par un routeur CentOS Stream 8.

Par exemple, pour les sous-réseaux internes j’ai mis de l’Open vSwitch. Pour acheminer les requètes HTTP & HTTPS, j’ai mis en place Traefik. Pour héberger mes services, j’ai déployé Kubernetes. Ça, je le garde pour un prochain article !