[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Le cache écoute sur toutes les adresses de l'hôte. Une requête qui le
|
|
|
|
|
// désigne LUI — son port, sur une adresse de cette machine — serait relayée
|
|
|
|
|
// vers sa propre écoute, qui la relaierait à son tour : chaque tour ouvre une
|
|
|
|
|
// connexion de plus, et la boucle épuise les descripteurs jusqu'à arrêter le
|
|
|
|
|
// service entier. Ce fichier la reconnaît et la refuse.
|
|
|
|
|
|
|
|
|
|
// viseLeCache dit si une URL désigne le cache lui-même.
|
|
|
|
|
//
|
|
|
|
|
// Le port d'abord : il ne coûte rien, et seul un port d'écoute du cache peut
|
|
|
|
|
// boucler. L'hôte ensuite — « localhost », une adresse de bouclage, une
|
|
|
|
|
// adresse non spécifiée ou celle d'une interface de cette machine. Un NOM
|
|
|
|
|
// n'est résolu que dans ce cas rare où le port concorde : le résoudre à
|
|
|
|
|
// chaque requête coûterait un aller-retour DNS à tout le trafic.
|
|
|
|
|
func (p *Proxy) viseLeCache(u *url.URL) bool {
|
|
|
|
|
if p == nil || len(p.Ecoutes) == 0 || u == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
port := portDe(u)
|
|
|
|
|
concorde := false
|
|
|
|
|
for _, e := range p.Ecoutes {
|
|
|
|
|
if e == port {
|
|
|
|
|
concorde = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !concorde {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
hote := strings.ToLower(strings.TrimSuffix(u.Hostname(), "."))
|
|
|
|
|
if hote == "localhost" || strings.HasSuffix(hote, ".localhost") {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if ip := net.ParseIP(hote); ip != nil {
|
|
|
|
|
return estAdresseLocale(ip)
|
|
|
|
|
}
|
|
|
|
|
ctx, annule := context.WithTimeout(context.Background(), 2*time.Second)
|
|
|
|
|
defer annule()
|
|
|
|
|
ips, err := net.DefaultResolver.LookupIPAddr(ctx, hote)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, a := range ips {
|
|
|
|
|
if estAdresseLocale(a.IP) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// portDe rend le port d'une URL, déduit du schéma quand elle n'en porte pas,
|
|
|
|
|
// et -1 quand il ne se déduit pas.
|
|
|
|
|
func portDe(u *url.URL) int {
|
|
|
|
|
if brut := u.Port(); brut != "" {
|
|
|
|
|
n, err := strconv.Atoi(brut)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
switch strings.ToLower(u.Scheme) {
|
|
|
|
|
case "https":
|
|
|
|
|
return 443
|
|
|
|
|
case "http":
|
|
|
|
|
return 80
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// estAdresseLocale dit si une adresse joint cette machine : bouclage,
|
|
|
|
|
// adresse non spécifiée — qu'une connexion traite comme l'hôte local — ou
|
|
|
|
|
// adresse de l'une de ses interfaces.
|
|
|
|
|
func estAdresseLocale(ip net.IP) bool {
|
|
|
|
|
if ip.IsLoopback() || ip.IsUnspecified() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
adresses, err := net.InterfaceAddrs()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, a := range adresses {
|
|
|
|
|
if n, ok := a.(*net.IPNet); ok && n.IP.Equal(ip) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// boucle répond « 508 Loop Detected » sans rien demander à personne, et le
|
|
|
|
|
// journal le compte comme une erreur. Le message est inerte pour un shell,
|
|
|
|
|
// comme celui du hors-ligne : il peut finir dans « curl … | bash ».
|
|
|
|
|
func (p *Proxy) boucle(
|
|
|
|
|
w http.ResponseWriter, u *url.URL, class Class, method, client string,
|
|
|
|
|
) {
|
|
|
|
|
msg := enCommentaire(fmt.Sprintf(
|
[ADD] cache qemu : messages du binaire en anglais ou en français
Le binaire ne parlait que français alors que le menu suit EL_LANG : un
opérateur anglophone lisait un statut qu'il ne comprenait pas. Chaque message
humain passe par T(), dont la clé EST le texte français, et un catalogue
anglais le traduit ; la langue vient de --lang, puis d'EL_LANG, le français
restant le défaut. Le menu demande le français pour ce qu'il RELIT, ses
motifs ne connaissant que cette langue. Un test refuse un message sans
traduction, une traduction orpheline et un verbe de format qui diverge.
--- EN ---
The binary spoke French only while the menu follows EL_LANG: an English
reader got a status he could not read. Every human message goes through T(),
whose key IS the French text, and an English catalogue translates it; the
language comes from --lang, then EL_LANG, French remaining the default. The
menu asks for French for what it READS BACK, its patterns knowing that
language alone. A test refuses a message without translation, an orphan
translation, and a format verb that drifts.
Assisted-by: Claude Opus 5
2026-09-16 02:59:01 -04:00
|
|
|
T("erplibre_go_qemu_cache : requête adressée au cache lui-même, refusée.\n"+
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
" demandé : %s\n"+
|
[ADD] cache qemu : messages du binaire en anglais ou en français
Le binaire ne parlait que français alors que le menu suit EL_LANG : un
opérateur anglophone lisait un statut qu'il ne comprenait pas. Chaque message
humain passe par T(), dont la clé EST le texte français, et un catalogue
anglais le traduit ; la langue vient de --lang, puis d'EL_LANG, le français
restant le défaut. Le menu demande le français pour ce qu'il RELIT, ses
motifs ne connaissant que cette langue. Un test refuse un message sans
traduction, une traduction orpheline et un verbe de format qui diverge.
--- EN ---
The binary spoke French only while the menu follows EL_LANG: an English
reader got a status he could not read. Every human message goes through T(),
whose key IS the French text, and an English catalogue translates it; the
language comes from --lang, then EL_LANG, French remaining the default. The
menu asks for French for what it READS BACK, its patterns knowing that
language alone. A test refuses a message without translation, an orphan
translation, and a format verb that drifts.
Assisted-by: Claude Opus 5
2026-09-16 02:59:01 -04:00
|
|
|
"La relayer la renverrait vers cette même écoute, sans fin.\n"),
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
u))
|
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
|
|
|
w.Header().Set("X-ERPLibre-Cache", OutcomeError)
|
|
|
|
|
w.WriteHeader(http.StatusLoopDetected)
|
|
|
|
|
fmt.Fprint(w, msg)
|
|
|
|
|
|
|
|
|
|
p.record(accessLine{
|
|
|
|
|
Method: method, URL: u.String(), Class: class.String(),
|
|
|
|
|
Outcome: OutcomeError, Status: http.StatusLoopDetected,
|
|
|
|
|
Client: client,
|
|
|
|
|
})
|
[ADD] cache qemu : messages du binaire en anglais ou en français
Le binaire ne parlait que français alors que le menu suit EL_LANG : un
opérateur anglophone lisait un statut qu'il ne comprenait pas. Chaque message
humain passe par T(), dont la clé EST le texte français, et un catalogue
anglais le traduit ; la langue vient de --lang, puis d'EL_LANG, le français
restant le défaut. Le menu demande le français pour ce qu'il RELIT, ses
motifs ne connaissant que cette langue. Un test refuse un message sans
traduction, une traduction orpheline et un verbe de format qui diverge.
--- EN ---
The binary spoke French only while the menu follows EL_LANG: an English
reader got a status he could not read. Every human message goes through T(),
whose key IS the French text, and an English catalogue translates it; the
language comes from --lang, then EL_LANG, French remaining the default. The
menu asks for French for what it READS BACK, its patterns knowing that
language alone. A test refuses a message without translation, an orphan
translation, and a format verb that drifts.
Assisted-by: Claude Opus 5
2026-09-16 02:59:01 -04:00
|
|
|
log.Printf(T("boucle refusée : %s vise le cache lui-même"), u)
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// memeAdresse compare deux « hôte:port » par leur VALEUR : « ::ffff:a.b.c.d »
|
|
|
|
|
// et « a.b.c.d » désignent la même machine, et une comparaison de textes les
|
|
|
|
|
// séparerait.
|
|
|
|
|
func memeAdresse(a, b string) bool {
|
|
|
|
|
ha, pa, err := net.SplitHostPort(a)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
hb, pb, err := net.SplitHostPort(b)
|
|
|
|
|
if err != nil || pa != pb {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
ia, ib := net.ParseIP(ha), net.ParseIP(hb)
|
|
|
|
|
if ia == nil || ib == nil {
|
|
|
|
|
return strings.EqualFold(ha, hb)
|
|
|
|
|
}
|
|
|
|
|
return ia.Equal(ib)
|
|
|
|
|
}
|