[FIX] cache qemu : une rafale de coupures ne condamne plus un hôte
A package manager opens several connections to one repository at once and closes those it does not use. The cache saw three cut handshakes in the same second, counted three refusals, and moved the host to an opaque tunnel, which never consults the store. A client then waited four hours on that tunnel, holding a socket the far end had closed, until its own deadline killed it. Cuts closer than two seconds now count as one incident; a client that really rejects the authority retries seconds later and is banned as before. A closing tunnel is logged with its duration and bytes each way. --- FR --- Un gestionnaire de paquets ouvre plusieurs connexions de front vers un dépôt et ferme celles dont il ne se sert pas. Le cache voyait trois poignées de main coupées dans la même seconde, comptait trois refus, et passait l'hôte en tunnel opaque, qui ne consulte jamais le magasin. Un client attendait alors quatre heures sur ce tunnel, sur un socket que l'autre bout avait refermé, jusqu'à son propre délai. Deux coupures à moins de deux secondes comptent désormais pour un incident ; un client qui rejette vraiment l'autorité réessaie et reste condamné. La fin d'un tunnel est journalisée. Assisted-by: Claude Opus 5
This commit is contained in:
parent
156956a1f0
commit
76ea0a2707
4 changed files with 135 additions and 6 deletions
|
|
@ -134,6 +134,7 @@ var anglais = map[string]string{
|
|||
"tunnel impossible pour %q : destination inconnue (%v)": "tunnel impossible for %q: unknown destination (%v)",
|
||||
"tunnel refusé pour %q : connexion non détournée, sa destination %s est cette écoute même": "tunnel refused for %q: connection not redirected, its destination %s is this very listener",
|
||||
"tunnel vers %s : %v ; %s est déchiffré à la place": "tunnel to %s: %v; %s is decrypted instead",
|
||||
"tunnel vers %s refermé après %s : %s vers l'amont, %s vers l'invité": "tunnel to %s closed after %s: %s upstream, %s to the guest",
|
||||
"tunnel vers %s : %v": "tunnel to %s: %v",
|
||||
"ce n'est pas une poignée de main TLS (type %d)": "not a TLS handshake (type %d)",
|
||||
"longueur d'enregistrement invraisemblable : %d": "implausible record length: %d",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const version = "0.2.11"
|
||||
const version = "0.2.12"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -233,8 +234,27 @@ type Refusals struct {
|
|||
// Seuil : combien de poignées de main de suite doivent échouer avant de
|
||||
// conclure. Nul, la valeur par défaut s'applique.
|
||||
Seuil int
|
||||
// Rafale : deux coupures plus rapprochées que cela ne comptent que pour
|
||||
// une. Nulle, la valeur par défaut s'applique ; négative, chaque coupure
|
||||
// compte, ce dont un test se sert pour condamner sans attendre.
|
||||
Rafale time.Duration
|
||||
// Maintenant rend l'instant courant. Nulle, time.Now : un test injecte
|
||||
// son horloge pour espacer des coupures sans dormir.
|
||||
Maintenant func() time.Time
|
||||
// dernier : quand a été COMPTÉE la dernière coupure d'un hôte, ce qui
|
||||
// distingue une rafale d'un échec qui se répète.
|
||||
dernier map[string]time.Time
|
||||
}
|
||||
|
||||
// RafaleParDefaut : en deçà, deux poignées de main coupées sont le MÊME
|
||||
// incident.
|
||||
//
|
||||
// apt ouvre plusieurs connexions de front vers un dépôt et ferme celles dont
|
||||
// il ne se sert pas : trois coupures dans la même seconde atteignaient le
|
||||
// seuil sans que rien n'ait rejeté notre certificat. Un client qui refuse
|
||||
// vraiment réessaie et échoue encore, à des secondes de là.
|
||||
const RafaleParDefaut = 2 * time.Second
|
||||
|
||||
// SeuilParDefaut : trois échecs de suite avant de renoncer à déchiffrer.
|
||||
//
|
||||
// Un client qui ne nous fera jamais confiance échoue à CHAQUE fois ; une
|
||||
|
|
@ -269,8 +289,10 @@ func NewRefusals(static []string) *Refusals {
|
|||
declares: map[string]bool{},
|
||||
apprises: map[string]refus{},
|
||||
echecs: map[string]int{},
|
||||
dernier: map[string]time.Time{},
|
||||
Oubli: OubliParDefaut,
|
||||
Seuil: SeuilParDefaut,
|
||||
Rafale: RafaleParDefaut,
|
||||
}
|
||||
for _, h := range static {
|
||||
if h = strings.TrimSpace(strings.ToLower(h)); h != "" {
|
||||
|
|
@ -309,7 +331,7 @@ func (r *Refusals) Has(host string) bool {
|
|||
if vu.alerte {
|
||||
return true
|
||||
}
|
||||
if r.Oubli > 0 && time.Since(vu.quand) >= r.Oubli {
|
||||
if r.Oubli > 0 && r.maintenant().Sub(vu.quand) >= r.Oubli {
|
||||
r.oublier(host)
|
||||
return false
|
||||
}
|
||||
|
|
@ -322,6 +344,7 @@ func (r *Refusals) Has(host string) bool {
|
|||
func (r *Refusals) oublier(host string) {
|
||||
delete(r.apprises, host)
|
||||
delete(r.echecs, host)
|
||||
delete(r.dernier, host)
|
||||
}
|
||||
|
||||
// Echec note une poignée de main manquée et dit si l'hôte passe en tunnel.
|
||||
|
|
@ -343,6 +366,18 @@ func (r *Refusals) Echec(host string, raison error) bool {
|
|||
seuil = SeuilParDefaut
|
||||
}
|
||||
alerte := estRefusTLS(raison)
|
||||
maintenant := r.maintenant()
|
||||
// Une rafale ne compte qu'une fois. Sans cela, un client qui ouvre
|
||||
// plusieurs connexions de front et ferme les inutiles atteint le seuil
|
||||
// seul : l'hôte passe en tunnel, le magasin cesse de le servir, et le
|
||||
// client qui attend sur ce tunnel n'a plus rien pour s'en sortir.
|
||||
if !alerte {
|
||||
if precedent, vu := r.dernier[host]; vu &&
|
||||
maintenant.Sub(precedent) < r.rafale() {
|
||||
return false
|
||||
}
|
||||
r.dernier[host] = maintenant
|
||||
}
|
||||
r.echecs[host]++
|
||||
if !alerte && r.echecs[host] < seuil {
|
||||
return false
|
||||
|
|
@ -354,10 +389,27 @@ func (r *Refusals) Echec(host string, raison error) bool {
|
|||
// La NATURE est retenue avec l'instant : elle décide si ce refus se
|
||||
// rouvrira. Un seuil atteint sur des coupures reste un soupçon, même
|
||||
// répété trois fois.
|
||||
r.apprises[host] = refus{quand: time.Now(), alerte: alerte}
|
||||
r.apprises[host] = refus{quand: maintenant, alerte: alerte}
|
||||
return true
|
||||
}
|
||||
|
||||
// maintenant rend l'instant courant, celui de l'horloge injectée s'il y en a.
|
||||
func (r *Refusals) maintenant() time.Time {
|
||||
if r.Maintenant != nil {
|
||||
return r.Maintenant()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// rafale rend la distance en deçà de laquelle deux coupures n'en font qu'une.
|
||||
// Négative, elle les fait toutes compter.
|
||||
func (r *Refusals) rafale() time.Duration {
|
||||
if r.Rafale != 0 {
|
||||
return r.Rafale
|
||||
}
|
||||
return RafaleParDefaut
|
||||
}
|
||||
|
||||
// Reussite efface le compte d'un hôte : la coupure d'avant n'était qu'un
|
||||
// incident, et deux incidents éloignés ne doivent pas s'additionner jusqu'au
|
||||
// seuil.
|
||||
|
|
@ -365,6 +417,7 @@ func (r *Refusals) Reussite(host string) {
|
|||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.echecs, host)
|
||||
delete(r.dernier, host)
|
||||
}
|
||||
|
||||
func (r *Refusals) List() []string {
|
||||
|
|
@ -375,7 +428,7 @@ func (r *Refusals) List() []string {
|
|||
out = append(out, h)
|
||||
}
|
||||
for h, vu := range r.apprises {
|
||||
if !vu.alerte && r.Oubli > 0 && time.Since(vu.quand) >= r.Oubli {
|
||||
if !vu.alerte && r.Oubli > 0 && r.maintenant().Sub(vu.quand) >= r.Oubli {
|
||||
r.oublier(h)
|
||||
continue
|
||||
}
|
||||
|
|
@ -596,10 +649,28 @@ func (t *TLSFront) tunnel(c net.Conn, host string) bool {
|
|||
Client: clientDe(c.RemoteAddr().String()),
|
||||
})
|
||||
|
||||
// Un tunnel qui se referme ne laissait AUCUNE trace : le client qui
|
||||
// attendait dessus paraissait bloqué sans cause, et rien au journal ne
|
||||
// disait lequel des deux bouts s'était taire. La durée et les octets par
|
||||
// direction le disent — un tunnel qui meurt sans avoir rien rendu à
|
||||
// l'invité est l'empreinte d'un amont qui coupe.
|
||||
debut := time.Now()
|
||||
var versAmont, versClient atomic.Int64
|
||||
done := make(chan struct{}, 2)
|
||||
go func() { io.Copy(up, c); done <- struct{}{} }()
|
||||
go func() { io.Copy(c, up); done <- struct{}{} }()
|
||||
go func() {
|
||||
n, _ := io.Copy(up, c)
|
||||
versAmont.Store(n)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
n, _ := io.Copy(c, up)
|
||||
versClient.Store(n)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
<-done
|
||||
log.Printf(T("tunnel vers %s refermé après %s : %s vers l'amont, %s vers l'invité"),
|
||||
dst, time.Since(debut).Round(time.Second),
|
||||
HumanBytes(versAmont.Load()), HumanBytes(versClient.Load()))
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ func TestPremierEnregistrementNonTLS(t *testing.T) {
|
|||
// produit une VM qui démarre et coupe. Seule la répétition les sépare.
|
||||
func TestUneCoupureIsoleeNeCondamnePas(t *testing.T) {
|
||||
r := NewRefusals(nil)
|
||||
r.Rafale = -1 // chaque coupure compte : c'est le SEUIL qu'on éprouve ici
|
||||
coupure := errors.New("read: connection reset by peer")
|
||||
for i := 1; i < r.Seuil; i++ {
|
||||
if r.Echec("miroir.example", coupure) {
|
||||
|
|
@ -223,9 +224,14 @@ func TestUneCoupureIsoleeNeCondamnePas(t *testing.T) {
|
|||
|
||||
func TestUneCoupureRepeteeFinitParCondamner(t *testing.T) {
|
||||
r := NewRefusals(nil)
|
||||
// Des coupures ESPACÉES, telles qu'un client qui réessaie les produit :
|
||||
// l'horloge avance entre chacune, sans faire dormir le test.
|
||||
horloge := time.Now()
|
||||
r.Maintenant = func() time.Time { return horloge }
|
||||
coupure := errors.New("EOF")
|
||||
for i := 0; i < r.Seuil; i++ {
|
||||
r.Echec("npm.example", coupure)
|
||||
horloge = horloge.Add(10 * time.Second)
|
||||
}
|
||||
if !r.Has("npm.example") {
|
||||
t.Error("un client qui échoue à chaque fois n'est jamais mis en" +
|
||||
|
|
@ -244,6 +250,7 @@ func TestUneCoupureRepeteeFinitParCondamner(t *testing.T) {
|
|||
func TestUnRefusDeTransportSeRouvre(t *testing.T) {
|
||||
r := NewRefusals(nil)
|
||||
r.Oubli = 50 * time.Millisecond
|
||||
r.Rafale = -1
|
||||
coupure := errors.New("local error: tls: bad record MAC")
|
||||
for i := 0; i < r.Seuil; i++ {
|
||||
r.Echec("miroir.example", coupure)
|
||||
|
|
@ -275,6 +282,7 @@ func TestLOubliRemetLeCompteAZero(t *testing.T) {
|
|||
t.Run(nom, func(t *testing.T) {
|
||||
r := NewRefusals(nil)
|
||||
r.Oubli = 50 * time.Millisecond
|
||||
r.Rafale = -1
|
||||
for i := 0; i < r.Seuil; i++ {
|
||||
r.Echec("registre.example", io.EOF)
|
||||
}
|
||||
|
|
@ -307,6 +315,7 @@ func TestUnTunnelSansAmontSeRabatSurLeDechiffrement(t *testing.T) {
|
|||
|
||||
const hote = "registre.example.invalid"
|
||||
r := NewRefusals(nil)
|
||||
r.Rafale = -1
|
||||
for i := 0; i < r.Seuil; i++ {
|
||||
r.Echec(hote, io.EOF)
|
||||
}
|
||||
|
|
@ -367,6 +376,7 @@ func TestLOubliEstActifParDefaut(t *testing.T) {
|
|||
// s'additionner jusqu'au seuil.
|
||||
func TestUneReussiteEffaceLeCompte(t *testing.T) {
|
||||
r := NewRefusals(nil)
|
||||
r.Rafale = -1
|
||||
coupure := errors.New("EOF")
|
||||
for i := 1; i < r.Seuil; i++ {
|
||||
r.Echec("h.example", coupure)
|
||||
|
|
@ -380,6 +390,53 @@ func TestUneReussiteEffaceLeCompte(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Une RAFALE est un seul incident. apt ouvre plusieurs connexions de front
|
||||
// vers un dépôt et ferme celles dont il ne se sert pas : trois coupures dans
|
||||
// la même seconde condamnaient l'hôte sans que rien n'ait rejeté notre
|
||||
// certificat. Le client repartait alors sur un tunnel opaque, et son
|
||||
// « apt-get update » y attendait jusqu'au délai — quatre heures.
|
||||
func TestUneRafaleDeCoupuresNeCondamnePas(t *testing.T) {
|
||||
r := NewRefusals(nil)
|
||||
horloge := time.Now()
|
||||
r.Maintenant = func() time.Time { return horloge }
|
||||
coupure := errors.New("read: connection reset by peer")
|
||||
|
||||
// Dix connexions fermées dans le même instant : un seul incident.
|
||||
for i := 0; i < 10; i++ {
|
||||
if r.Echec("deb.example", coupure) {
|
||||
t.Fatalf("condamné sur la coupure %d d'une même rafale", i+1)
|
||||
}
|
||||
}
|
||||
if r.Has("deb.example") {
|
||||
t.Fatal("une rafale simultanée condamne l'hôte")
|
||||
}
|
||||
|
||||
// Le temps passe : les coupures suivantes sont d'autres incidents, et le
|
||||
// seuil finit par tomber.
|
||||
for i := 1; i < r.Seuil; i++ {
|
||||
horloge = horloge.Add(RafaleParDefaut + time.Second)
|
||||
r.Echec("deb.example", coupure)
|
||||
}
|
||||
if !r.Has("deb.example") {
|
||||
t.Error("des coupures espacées ne condamnent plus : un client qui" +
|
||||
" rejette vraiment notre autorité ne passerait jamais en tunnel")
|
||||
}
|
||||
}
|
||||
|
||||
// Une ALERTE tranche tout de suite : le client a REGARDÉ notre certificat.
|
||||
// La rafale ne la retient pas : dix alertes d'affilée valent une décision.
|
||||
func TestUneAlerteTrancheMemeEnRafale(t *testing.T) {
|
||||
r := NewRefusals(nil)
|
||||
horloge := time.Now()
|
||||
r.Maintenant = func() time.Time { return horloge }
|
||||
if !r.Echec("epingleur.example", errRefus) {
|
||||
t.Fatal("une alerte ne tranche pas dans le même instant qu'une autre")
|
||||
}
|
||||
if !r.Has("epingleur.example") {
|
||||
t.Error("l'alerte n'est pas retenue")
|
||||
}
|
||||
}
|
||||
|
||||
// Une ALERTE tranche tout de suite : le client a REGARDÉ notre certificat.
|
||||
func TestUneAlerteCondamneDesLePremierEchec(t *testing.T) {
|
||||
r := NewRefusals(nil)
|
||||
|
|
|
|||
Loading…
Reference in a new issue