Commit 22d2c7f0 authored by Kourser's avatar Kourser
Browse files

Une plage se redirige mal, un port se redirige

LiveKit écoutait sur cent ports UDP contigus. La plage a l'air plus
généreuse ; elle oblige surtout chaque équipement du chemin à porter une
redirection de plage, et c'est ce qui se configure le plus mal.

Constaté sur une installation réelle, derrière deux NAT : la règle
« 50200-50300 vers 50200 » livrait tout le trafic sur le seul 50200,
quand le navigateur visait 50237. La signalisation réussissait, le jeton
était accepté, la salle s'ouvrait — et personne ne voyait personne. Le
navigateur ne disait rien de plus utile que « ICE failed ». L'appel ne
tenait que par ICE-TCP, le pire des chemins retenus.

LiveKit sait multiplexer tout son média sur un port unique. Celui-là se
redirige comme la signalisation ou le TURN, c'est-à-dire de la façon qui
marche déjà partout. Il épargne aussi cent processus « docker-proxy ».

La composition garde un repli sur l'ancien nom : un .env non migré porte
une plage dont le premier numéro est celui que LiveKit écoute désormais,
si bien qu'un « git pull » sans installateur ne casse personne.

Ce repli est emboîté, et deux aides de l'installateur dépliaient
« ${VAR:-défaut} » avec une expression qui s'arrête au premier « } » —
elles rendaient du texte au lieu d'un nombre. « ports_composition »
aurait cessé de voir le port média, c'est-à-dire l'aide même qui détecte
les conflits de ports. Les deux savent désormais déplier.

L'installateur migre la valeur, et non seulement le nom : la table de
renommage conserve les valeurs, ce qui aurait laissé « 50200-50300 » sous
un nom qui promet un numéro. Il migre « livekit.local.yaml » dans le même
geste — séparer les deux redonnerait exactement la panne ci-dessus.

Quatre garanties nouvelles tiennent l'accord des deux fichiers, éprouvées
en cassant chaque condition plutôt qu'en les regardant passer au vert.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent febb485d
Loading
Loading
Loading
Loading
Loading
+13 −1
Changes for .env.example: 13 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -111,11 +111,23 @@ LIVEKIT_EGRESS=oui
# EN   These numbers also live in LiveKit's own configuration file. Changing
# EN   them here alone is not enough: LIVEKIT_CONFIG must name a file carrying
# EN   the same ones, which the installer writes for you.
#
# LIVEKIT_PORT_MEDIA est un port UNIQUE, et non une plage. LiveKit y multiplexe
# tout le média. Une plage se redirige mal : sur un hébergement à plusieurs
# NAT, la règle « 50200-50300 vers 50200 » livre tout sur le seul 50200, le
# navigateur vise un autre numéro, et l'appel échoue sur « ICE failed » sans
# que rien n'indique où chercher. Un port unique se redirige comme les autres.
#
# EN — LIVEKIT_PORT_MEDIA is a SINGLE port, not a range. LiveKit multiplexes
# EN   all media on it. A range forwards badly: on a multi-NAT host, the rule
# EN   "50200-50300 to 50200" delivers everything on 50200 alone, the browser
# EN   aims at another number, and the call fails on "ICE failed" with nothing
# EN   to say where to look. A single port forwards like any other.
LIVEKIT_PORT=7880
LIVEKIT_PORT_RTC_TCP=7881
LIVEKIT_PORT_TURN=3478
LIVEKIT_PORT_TURN_TLS=5349
LIVEKIT_PORTS_MEDIA=50000-50100
LIVEKIT_PORT_MEDIA=50000
LIVEKIT_CONFIG=../docker/livekit.yaml

# ── Enregistreur : par où lui parvient le travail ──────────────────────────
+43 −0
Changes for apps/api/src/verification/composition.ts: 43 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -88,4 +88,47 @@ export async function verifierComposition(r: Rapport): Promise<void> {
  r.verifie("EX-26",
    `les ${laisseesVides.length} variables que la composition laisse vides sont tolérées`,
    laisseesVides.length > 0 && motif === "", motif);

  await verifierPortMedia(compose, r);
}

/**
 * Le port média, des deux côtés, et un seul.
 *
 * Ce numéro vit à deux endroits : la configuration de LiveKit, qui décide où il
 * écoute, et la publication de la composition, qui décide ce qui lui parvient.
 * Les désaccorder donne la panne la plus coûteuse de la famille — la salle
 * s'ouvre, les jetons sont acceptés, la signalisation réussit, et personne ne
 * voit personne. Le navigateur ne dit rien de plus utile que « ICE failed ».
 *
 * Une plage a longtemps tenu la place d'un port. Elle a l'air plus généreuse ;
 * elle oblige surtout chaque équipement du chemin à porter une redirection de
 * plage, et c'est ce qui se configure le plus mal. Vu sur une installation
 * réelle à deux NAT : « 50200-50300 vers 50200 » livrait tout le trafic sur le
 * seul 50200, quand le navigateur visait 50237.
 *
 * Le contrôle refuse donc aussi le retour de la plage, et pas seulement le
 * désaccord des deux fichiers.
 */
async function verifierPortMedia(compose: string, r: Rapport): Promise<void> {
  const livekit = await readFile(`${racine}docker/livekit.yaml`, "utf8");

  const declare = /^\s*udp_port:\s*(\d+)\s*$/m.exec(
    livekit.slice(livekit.indexOf("rtc:"), livekit.indexOf("turn:")));
  r.verifie("EF-803", "LiveKit multiplexe son média sur un port unique",
    declare !== null, declare?.[1] ?? "aucun udp_port sous « rtc: »");

  r.verifie("EF-803", "et non sur une plage, qui se redirige mal",
    !/^\s*port_range_(start|end):/m.test(livekit));

  // Le repli emboîté « ${NEUF:-${VIEUX:-x}} » sert un .env non migré ; c'est le
  // littéral au fond qui dit ce que la composition publie sans .env.
  const publie = /- "\$\{LIVEKIT_PORT_MEDIA:-(?:\$\{LIVEKIT_PORTS_MEDIA:-)?(\d+)\}*[^"]*\/udp"/
    .exec(compose);
  r.verifie("EF-803", "la composition publie ce port en UDP",
    publie !== null, publie?.[1] ?? "aucune publication UDP du port média");

  r.verifie("EF-803", "et c'est le même numéro des deux côtés",
    declare !== null && publie !== null && declare[1] === publie[1],
    `livekit.yaml ${declare?.[1] ?? "—"} / compose ${publie?.[1] ?? "—"}`);
}
+5 −1
Changes for docker/compose.yaml: 5 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -138,7 +138,11 @@ services:
      # En production, publiez ce port sur 443 : c'est le dernier chemin quand
      # le pare-feu du client bloque tout le reste (EF-804).
      - "${LIVEKIT_PORT_TURN_TLS:-5349}:${LIVEKIT_PORT_TURN_TLS:-5349}"
      - "${LIVEKIT_PORTS_MEDIA:-50000-50100}:${LIVEKIT_PORTS_MEDIA:-50000-50100}/udp"
      # Un seul port UDP, multiplexé. L'ancien nom est encore lu : un .env
      # qui n'a pas été migré porte une plage, dont le premier numéro est
      # celui que LiveKit écoute désormais — la publication reste juste,
      # simplement plus large que nécessaire.
      - "${LIVEKIT_PORT_MEDIA:-${LIVEKIT_PORTS_MEDIA:-50000}}:${LIVEKIT_PORT_MEDIA:-${LIVEKIT_PORTS_MEDIA:-50000}}/udp"

  # L'enregistrement des réunions (EF-806). Démarré par défaut pour que la
  # visioconférence soit entière : une visio qui ne s'enregistre pas est une
+13 −2
Changes for docker/livekit.yaml: 13 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -9,8 +9,19 @@ log_level: info

rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 50100
  # Un seul port UDP, multiplexé, plutôt qu'une plage.
  #
  # LiveKit sait faire les deux. La plage — cent ports contigus — a l'air plus
  # généreuse ; elle coûte en réalité une redirection de plage sur CHAQUE
  # équipement du chemin, et une redirection de plage est ce qui se configure
  # le plus mal. Sur un hébergement à deux NAT, la règle « 50200-50300 vers
  # 50200 » livre tout le trafic sur le seul port 50200 : le navigateur vise
  # 50237, personne ne répond, et ICE échoue sans rien dire d'utile.
  #
  # Un port unique se redirige comme n'importe quel service, de la même façon
  # que la signalisation ou le TURN — c'est-à-dire de la façon qui marche déjà
  # partout. Il épargne aussi cent processus « docker-proxy » sur l'hôte.
  udp_port: 50000
  use_external_ip: false

# Relais TURN.
+62 −31
Changes for outils/installer.sh: 62 added lines, 31 removed lines.
Original line number Diff line number Diff line
@@ -305,6 +305,44 @@ migrer_env() {
    fi
  done

  # ── Le cas que la table ne sait pas porter ──────────────────────────────
  #
  # LIVEKIT_PORTS_MEDIA tenait une plage, LIVEKIT_PORT_MEDIA tient un port.
  # Le renommage générique conserve la valeur ; ici elle doit changer de forme,
  # sinon le .env porterait « 50200-50300 » sous un nom qui promet un numéro,
  # et la composition publierait une plage que LiveKit n'écoute plus.
  #
  # On garde le premier numéro de la plage. C'est celui que LiveKit multiplexe
  # désormais, et il était déjà ouvert de bout en bout sur le chemin — le
  # correctif ne demande donc aucune règle de pare-feu nouvelle.
  if grep -q '^LIVEKIT_PORTS_MEDIA=' .env && ! grep -q '^LIVEKIT_PORT_MEDIA=' .env; then
    _plage="$(valeur_env LIVEKIT_PORTS_MEDIA)"
    poser_valeur LIVEKIT_PORTS_MEDIA "${_plage%%-*}"
    sed 's/^LIVEKIT_PORTS_MEDIA=/LIVEKIT_PORT_MEDIA=/' .env > .env.migration
    cat .env.migration > .env
    rm -f .env.migration
    renommees="$renommees LIVEKIT_PORTS_MEDIA→LIVEKIT_PORT_MEDIA"

    # Le fichier de configuration doit suivre la variable. Les deux portent le
    # même numéro, de part et d'autre de la publication ; migrer l'un sans
    # l'autre donne une salle qui s'ouvre et où personne ne se voit — la panne
    # la plus coûteuse, parce qu'elle a l'air d'une réussite.
    #
    # On ne réécrit pas le fichier : il porte les choix de la machine. On n'y
    # remplace que les deux lignes de la plage.
    if [ -f docker/livekit.local.yaml ] \
       && grep -q '^[[:space:]]*port_range_start:' docker/livekit.local.yaml; then
      awk -v p="${_plage%%-*}" '
        /^[[:space:]]*port_range_start:/ { print "  udp_port: " p; next }
        /^[[:space:]]*port_range_end:/   { next }
        { print }
      ' docker/livekit.local.yaml > docker/livekit.local.migration
      cat docker/livekit.local.migration > docker/livekit.local.yaml
      rm -f docker/livekit.local.migration
      renommees="$renommees livekit.local.yaml:plage→port"
    fi
  fi

  # ── Ajouter ce qui manque ───────────────────────────────────────────────
  #
  # Lecture par redirection et non par tube : un « while read » dans un tube
@@ -367,7 +405,8 @@ ports_composition() {
  # .env ne dit rien, et l'ignorer rendrait ce contrôle aveugle à LiveKit —
  # c'est-à-dire à la moitié des conflits qu'il existe pour attraper.
  sed -n 's/^[[:space:]]*-[[:space:]]*"\([^"]*\)".*$/\1/p' "$COMPOSE" |
  sed 's/\${[A-Za-z_][A-Za-z0-9_]*:-\([^}]*\)}/\1/g' |
  sed -e 's/\${[A-Za-z_][A-Za-z0-9_]*:-\([^}]*\)}/\1/g' \
      -e 's/\${[A-Za-z_][A-Za-z0-9_]*:-\([^}]*\)}/\1/g' |
  awk -F: '
    /\$\{/ { next }
    {
@@ -385,6 +424,16 @@ ports_composition() {
# et non répétées.
defaut_compose() {   # <NOM> <repli>
  _v="$(sed -n "s/.*\${$1:-\([^}]*\)}.*/\1/p" "$COMPOSE" | head -n 1)"
  # La composition emboîte un repli dans un autre — « ${NEUF:-${VIEUX:-x}} » —
  # pour qu'un .env non migré continue de fonctionner. L'extraction ci-dessus
  # s'arrête alors au premier « } » et rend « ${VIEUX:-x ». On déplie jusqu'au
  # littéral : c'est lui, la valeur qui vaut tant que le .env se tait.
  while :; do
    case "$_v" in
      '${'*':-'*) _v="${_v#*:-}" ;;
      *) break ;;
    esac
  done
  echo "${_v:-$2}"
}

@@ -459,23 +508,6 @@ premier_port_libre() { # <départ> [proto] — remonte jusqu'à en trouver un
  echo "$1"
}

# La plage média est contiguë et large : on en sonde les deux bords plutôt que
# les cent ports, ce qui suffit à écarter un voisin installé aux valeurs
# habituelles.
premiere_plage_libre() {   # <départ> <largeur>
  _d="$1"
  while [ "$_d" -lt 64000 ]; do
    _f=$((_d + $2))
    if [ -z "$(conflit_composition "$_d")" ] && [ -z "$(conflit_composition "$_f")" ] \
       && port_libre "$_d" udp && port_libre "$_f" udp; then
      echo "$_d-$_f"
      return
    fi
    _d=$((_d + $2 + 1))
  done
  echo "$1-$(($1 + $2))"
}

# Ce que le jeu de ports retenu heurte, une ligne par port pris.
ports_livekit_pris() {
  port_libre "$livekit_port" tcp \
@@ -486,8 +518,8 @@ ports_livekit_pris() {
    || dire "$livekit_port_turn — TURN UDP" "$livekit_port_turn — TURN UDP"
  port_libre "$livekit_port_turn_tls" tcp \
    || dire "$livekit_port_turn_tls — TURN TLS" "$livekit_port_turn_tls — TURN TLS"
  port_libre "${livekit_ports_media%-*}" udp \
    || dire "$livekit_ports_media — plage média" "$livekit_ports_media — media range"
  port_libre "$livekit_port_media" udp \
    || dire "$livekit_port_media — média UDP" "$livekit_port_media — media UDP"
}

# ── La langue de l'entretien ──────────────────────────────────────────────
@@ -1064,7 +1096,7 @@ if [ "$ecrire_env" = "oui" ]; then
  livekit_port_rtc_tcp="$(defaut_compose LIVEKIT_PORT_RTC_TCP 7881)"
  livekit_port_turn="$(defaut_compose LIVEKIT_PORT_TURN 3478)"
  livekit_port_turn_tls="$(defaut_compose LIVEKIT_PORT_TURN_TLS 5349)"
  livekit_ports_media="$(defaut_compose LIVEKIT_PORTS_MEDIA 50000-50100)"
  livekit_port_media="$(defaut_compose LIVEKIT_PORT_MEDIA 50000)"
  livekit_config="$(defaut_compose LIVEKIT_CONFIG ../docker/livekit.yaml)"
  livekit_redis="redis:6379"
  livekit_egress="non"
@@ -1122,14 +1154,14 @@ if [ "$ecrire_env" = "oui" ]; then
          dire "  Deux serveurs média sur une machine demandent des jeux de" \
               "  Two media servers on one machine need disjoint sets of ports —"
          dire "  ports disjoints — la signalisation, le RTC en TCP, les deux" \
               "  signalling, RTC over TCP, both TURN ports, and the media range."
          dire "  ports TURN, et la plage média. En voici un de libre :" \
               "  signalling, RTC over TCP, both TURN ports, and the media port."
          dire "  ports TURN, et le port média. En voici un de libre :" \
               "  Here is a free set:"
          livekit_port="$(premier_port_libre 7890 tcp)"
          livekit_port_rtc_tcp="$(premier_port_libre $((livekit_port + 1)) tcp)"
          livekit_port_turn="$(premier_port_libre 3479 udp)"
          livekit_port_turn_tls="$(premier_port_libre 5350 tcp)"
          livekit_ports_media="$(premiere_plage_libre 50200 100)"
          livekit_port_media="$(premier_port_libre 50200 udp)"
          echo
          dire "      signalisation   $livekit_port" \
               "      signalling      $livekit_port"
@@ -1137,8 +1169,8 @@ if [ "$ecrire_env" = "oui" ]; then
               "      RTC over TCP    $livekit_port_rtc_tcp"
          echo "      TURN UDP        $livekit_port_turn"
          echo "      TURN TLS        $livekit_port_turn_tls"
          dire "      plage média     $livekit_ports_media" \
               "      media range     $livekit_ports_media"
          dire "      média UDP       $livekit_port_media" \
               "      media UDP       $livekit_port_media"
          echo
          if ! oui_non_b "Ce jeu convient-il ?" "Does this set suit you?" "o"; then
            demander_b "Port de signalisation" "Signalling port" "$livekit_port"
@@ -1149,8 +1181,8 @@ if [ "$ecrire_env" = "oui" ]; then
            livekit_port_turn="$REPONSE"
            demander_b "Port TURN TLS" "TURN TLS port" "$livekit_port_turn_tls"
            livekit_port_turn_tls="$REPONSE"
            demander_b "Plage média (début-fin)" "Media range (start-end)" "$livekit_ports_media"
            livekit_ports_media="$REPONSE"
            demander_b "Port média UDP" "Media UDP port" "$livekit_port_media"
            livekit_port_media="$REPONSE"
            pris="$(ports_livekit_pris)"
            if [ -n "$pris" ]; then
              echo
@@ -1459,7 +1491,7 @@ LIVEKIT_PORT=$livekit_port
LIVEKIT_PORT_RTC_TCP=$livekit_port_rtc_tcp
LIVEKIT_PORT_TURN=$livekit_port_turn
LIVEKIT_PORT_TURN_TLS=$livekit_port_turn_tls
LIVEKIT_PORTS_MEDIA=$livekit_ports_media
LIVEKIT_PORT_MEDIA=$livekit_port_media
LIVEKIT_CONFIG=$livekit_config

# Where the recorder receives its work: the media server's Redis, and not ours
@@ -1564,8 +1596,7 @@ log_level: info

rtc:
  tcp_port: $livekit_port_rtc_tcp
  port_range_start: ${livekit_ports_media%-*}
  port_range_end: ${livekit_ports_media#*-}
  udp_port: $livekit_port_media
  use_external_ip: $livekit_ip_externe

turn: