500 lines
15 KiB
Go
500 lines
15 KiB
Go
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||
|
|
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/tls"
|
||
|
|
"crypto/x509"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"slices"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestCreationEtRelectureDeLAutorite(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
ca1, err := LoadOrCreateCA(dir)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("création : %v", err)
|
||
|
|
}
|
||
|
|
// Relire ne doit PAS régénérer : une autorité neuve invaliderait les
|
||
|
|
// certificats de toutes les VM déjà configurées.
|
||
|
|
ca2, err := LoadOrCreateCA(dir)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("relecture : %v", err)
|
||
|
|
}
|
||
|
|
if ca1.Fingerprint() != ca2.Fingerprint() {
|
||
|
|
t.Errorf("l'autorité a été régénérée :\n %s\n %s",
|
||
|
|
ca1.Fingerprint(), ca2.Fingerprint())
|
||
|
|
}
|
||
|
|
if !ca1.cert.IsCA {
|
||
|
|
t.Error("le certificat n'est pas une autorité")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// La clé permet de se faire passer pour n'importe quel site auprès d'une VM
|
||
|
|
// qui approuve l'autorité : elle ne doit être lisible que par son
|
||
|
|
// propriétaire.
|
||
|
|
func TestCleDeLAutoriteEnMode0600(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
if _, err := LoadOrCreateCA(dir); err != nil {
|
||
|
|
t.Fatalf("création : %v", err)
|
||
|
|
}
|
||
|
|
st, err := os.Stat(filepath.Join(dir, "ca.key"))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("clé absente : %v", err)
|
||
|
|
}
|
||
|
|
if mode := st.Mode().Perm(); mode != 0o600 {
|
||
|
|
t.Errorf("clé en %o, attendu 600", mode)
|
||
|
|
}
|
||
|
|
// Le certificat, lui, part dans les VM : il doit rester lisible.
|
||
|
|
stc, err := os.Stat(CertPath(dir))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("certificat absent : %v", err)
|
||
|
|
}
|
||
|
|
if mode := stc.Mode().Perm(); mode != 0o644 {
|
||
|
|
t.Errorf("certificat en %o, attendu 644", mode)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une feuille doit se vérifier CONTRE l'autorité, sans quoi l'invité la
|
||
|
|
// rejette malgré la confiance accordée.
|
||
|
|
func TestFeuilleVerifieeParLAutorite(t *testing.T) {
|
||
|
|
ca, err := LoadOrCreateCA(t.TempDir())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("autorité : %v", err)
|
||
|
|
}
|
||
|
|
crt, err := ca.leafFor("miroir.example")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("feuille : %v", err)
|
||
|
|
}
|
||
|
|
leaf, err := x509.ParseCertificate(crt.Certificate[0])
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("feuille illisible : %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
pool := x509.NewCertPool()
|
||
|
|
pool.AddCert(ca.cert)
|
||
|
|
if _, err := leaf.Verify(x509.VerifyOptions{
|
||
|
|
DNSName: "miroir.example",
|
||
|
|
Roots: pool,
|
||
|
|
}); err != nil {
|
||
|
|
t.Errorf("la feuille ne se vérifie pas : %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// La chaîne envoyée porte l'autorité après la feuille : un invité qui ne
|
||
|
|
// la connaît pas encore doit pouvoir la voir.
|
||
|
|
if len(crt.Certificate) != 2 {
|
||
|
|
t.Errorf("la chaîne porte %d certificats, attendu 2", len(crt.Certificate))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un hôte demandé deux fois ne coûte qu'une génération : une installation
|
||
|
|
// touche des dizaines d'hôtes.
|
||
|
|
func TestFeuilleGardeeEnMemoire(t *testing.T) {
|
||
|
|
ca, err := LoadOrCreateCA(t.TempDir())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("autorité : %v", err)
|
||
|
|
}
|
||
|
|
a, _ := ca.leafFor("miroir.example")
|
||
|
|
b, _ := ca.leafFor("miroir.example")
|
||
|
|
if a != b {
|
||
|
|
t.Error("la feuille est régénérée à chaque demande")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un SNI qui est une adresse IP ne peut pas aller dans un nom DNS.
|
||
|
|
func TestFeuillePourUneAdresseIP(t *testing.T) {
|
||
|
|
ca, err := LoadOrCreateCA(t.TempDir())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("autorité : %v", err)
|
||
|
|
}
|
||
|
|
crt, err := ca.leafFor("192.168.122.50")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("feuille : %v", err)
|
||
|
|
}
|
||
|
|
leaf, _ := x509.ParseCertificate(crt.Certificate[0])
|
||
|
|
if len(leaf.IPAddresses) != 1 {
|
||
|
|
t.Errorf("%d adresse(s) dans le certificat, attendu 1", len(leaf.IPAddresses))
|
||
|
|
}
|
||
|
|
if len(leaf.DNSNames) != 0 {
|
||
|
|
t.Errorf("une adresse IP est passée en nom DNS : %v", leaf.DNSNames)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// errRefus est l'alerte qu'envoie un client qui a REGARDÉ notre certificat.
|
||
|
|
var errRefus = errors.New("remote error: tls: unknown certificate authority")
|
||
|
|
|
||
|
|
// Le repli ne se déclare pas d'avance : un hôte qui refuse le certificat est
|
||
|
|
// retenu, et la requête suivante vers lui n'essaie plus de le déchiffrer.
|
||
|
|
func TestRefusRetenu(t *testing.T) {
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
if r.Has("api.example") {
|
||
|
|
t.Fatal("un hôte est exclu avant tout refus")
|
||
|
|
}
|
||
|
|
r.Echec("api.example", errRefus)
|
||
|
|
if !r.Has("api.example") {
|
||
|
|
t.Error("le refus n'est pas retenu")
|
||
|
|
}
|
||
|
|
// Deux fois le même hôte ne double pas l'entrée.
|
||
|
|
r.Echec("api.example", errRefus)
|
||
|
|
if n := len(r.List()); n != 1 {
|
||
|
|
t.Errorf("%d hôtes retenus, attendu 1", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un suffixe couvre un domaine entier, sous-domaines compris.
|
||
|
|
func TestExclusionParSuffixe(t *testing.T) {
|
||
|
|
r := NewRefusals([]string{".snapcraft.io"})
|
||
|
|
for _, h := range []string{"api.snapcraft.io", "dashboard.snapcraft.io"} {
|
||
|
|
if !r.Has(h) {
|
||
|
|
t.Errorf("%s n'est pas couvert par le suffixe", h)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if r.Has("snapcraft.io.example.com") {
|
||
|
|
t.Error("le suffixe attrape un domaine qui ne fait que le contenir")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Les hôtes dont l'épinglage est connu d'avance évitent de perdre une requête
|
||
|
|
// pour l'apprendre.
|
||
|
|
func TestExclusionsParDefaut(t *testing.T) {
|
||
|
|
r := NewRefusals(DefaultExclusions)
|
||
|
|
if !r.Has("api.snapcraft.io") {
|
||
|
|
t.Error("snapd n'est pas exclu d'avance, alors qu'il épingle")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// La casse d'un nom d'hôte n'a pas de sens en DNS.
|
||
|
|
func TestExclusionInsensibleALaCasse(t *testing.T) {
|
||
|
|
r := NewRefusals([]string{"API.Example"})
|
||
|
|
if !r.Has("api.example") {
|
||
|
|
t.Error("l'exclusion dépend de la casse")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// L'empreinte sert à vérifier de visu qu'une VM approuve BIEN cette autorité.
|
||
|
|
func TestEmpreinteLisible(t *testing.T) {
|
||
|
|
ca, err := LoadOrCreateCA(t.TempDir())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("autorité : %v", err)
|
||
|
|
}
|
||
|
|
fp := ca.Fingerprint()
|
||
|
|
if n := strings.Count(fp, ":"); n != 31 {
|
||
|
|
t.Errorf("%d séparateurs, attendu 31 pour un SHA-256", n)
|
||
|
|
}
|
||
|
|
if fp != strings.ToUpper(fp) {
|
||
|
|
t.Errorf("empreinte en minuscules : %s", fp)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un premier enregistrement qui n'est pas une poignée de main TLS est refusé
|
||
|
|
// tôt : le port du 443 détourné ne reçoit rien d'autre.
|
||
|
|
func TestPremierEnregistrementNonTLS(t *testing.T) {
|
||
|
|
if _, err := peekSNI([]byte{0x17, 0x03, 0x03, 0x00, 0x01, 0x00}); err == nil {
|
||
|
|
t.Error("un enregistrement qui n'est pas un handshake est accepté")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une coupure isolée ne condamne pas ; une coupure QUI SE RÉPÈTE, oui.
|
||
|
|
//
|
||
|
|
// Les deux se ressemblent au serveur : npm rejette notre certificat sans
|
||
|
|
// envoyer d'alerte, si bien qu'on ne voit qu'un EOF — exactement ce que
|
||
|
|
// produit une VM qui démarre et coupe. Seule la répétition les sépare.
|
||
|
|
func TestUneCoupureIsoleeNeCondamnePas(t *testing.T) {
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
coupure := errors.New("read: connection reset by peer")
|
||
|
|
for i := 1; i < r.Seuil; i++ {
|
||
|
|
if r.Echec("miroir.example", coupure) {
|
||
|
|
t.Fatalf("condamné dès l'échec %d, seuil %d", i, r.Seuil)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if r.Has("miroir.example") {
|
||
|
|
t.Error("condamné avant le seuil")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUneCoupureRepeteeFinitParCondamner(t *testing.T) {
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
coupure := errors.New("EOF")
|
||
|
|
for i := 0; i < r.Seuil; i++ {
|
||
|
|
r.Echec("npm.example", coupure)
|
||
|
|
}
|
||
|
|
if !r.Has("npm.example") {
|
||
|
|
t.Error("un client qui échoue à chaque fois n'est jamais mis en" +
|
||
|
|
" tunnel : son installation s'arrêtera là")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un soupçon se rouvre ; une décision, jamais.
|
||
|
|
//
|
||
|
|
// Trois coupures de suite condamnent l'hôte, et c'est voulu — mais elles ne
|
||
|
|
// disent rien de ce que le client pense de notre autorité. Le garder banni
|
||
|
|
// pour toujours prive le cache de tout ce qu'il détient pour lui : un tunnel
|
||
|
|
// recopie des octets sans consulter le magasin. Un miroir de distribution
|
||
|
|
// banni sur une rafale de flux corrompus renvoie alors à l'amont jusqu'à ce
|
||
|
|
// qui est en réserve.
|
||
|
|
func TestUnRefusDeTransportSeRouvre(t *testing.T) {
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
r.Oubli = 50 * time.Millisecond
|
||
|
|
coupure := errors.New("local error: tls: bad record MAC")
|
||
|
|
for i := 0; i < r.Seuil; i++ {
|
||
|
|
r.Echec("miroir.example", coupure)
|
||
|
|
}
|
||
|
|
if !r.Has("miroir.example") {
|
||
|
|
t.Fatal("le seuil ne condamne plus")
|
||
|
|
}
|
||
|
|
time.Sleep(60 * time.Millisecond)
|
||
|
|
if r.Has("miroir.example") {
|
||
|
|
t.Error("un soupçon de transport ne se rouvre jamais")
|
||
|
|
}
|
||
|
|
if slices.Contains(r.List(), "miroir.example") {
|
||
|
|
t.Error("un refus expiré figure encore dans la liste")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// L'oubli rouvre le doute ENTIER : un hôte rouvert qui coupe une fois n'est pas
|
||
|
|
// recondamné sur-le-champ. Le compte d'avant l'oubli, s'il survivait, ferait
|
||
|
|
// d'une coupure isolée — un processus qui se termine en abandonnant ses
|
||
|
|
// poignées de main — un nouveau bannissement de toute la durée d'oubli.
|
||
|
|
func TestLOubliRemetLeCompteAZero(t *testing.T) {
|
||
|
|
// Has et List constatent l'oubli chacun de leur côté : les deux doivent
|
||
|
|
// rouvrir le compte.
|
||
|
|
lectures := map[string]func(r *Refusals, h string) bool{
|
||
|
|
"Has": func(r *Refusals, h string) bool { return r.Has(h) },
|
||
|
|
"List": func(r *Refusals, h string) bool { return slices.Contains(r.List(), h) },
|
||
|
|
}
|
||
|
|
for nom, lire := range lectures {
|
||
|
|
t.Run(nom, func(t *testing.T) {
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
r.Oubli = 50 * time.Millisecond
|
||
|
|
for i := 0; i < r.Seuil; i++ {
|
||
|
|
r.Echec("registre.example", io.EOF)
|
||
|
|
}
|
||
|
|
time.Sleep(60 * time.Millisecond)
|
||
|
|
if lire(r, "registre.example") {
|
||
|
|
t.Fatal("le soupçon ne s'est pas rouvert")
|
||
|
|
}
|
||
|
|
if r.Echec("registre.example", io.EOF) {
|
||
|
|
t.Error("une seule coupure après l'oubli recondamne l'hôte")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un hôte en tunnel dont l'amont ne répond pas est DÉCHIFFRÉ sur la même
|
||
|
|
// connexion : le tunnel ne pourrait que couper le client, alors que le magasin
|
||
|
|
// détient peut-être ce qu'il demande.
|
||
|
|
func TestUnTunnelSansAmontSeRabatSurLeDechiffrement(t *testing.T) {
|
||
|
|
ca, err := LoadOrCreateCA(t.TempDir())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// Une adresse qui refuse : l'écoute est fermée aussitôt ouverte.
|
||
|
|
ferme, err := net.Listen("tcp", "127.0.0.1:0")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
coupee := ferme.Addr().String()
|
||
|
|
ferme.Close()
|
||
|
|
|
||
|
|
const hote = "registre.example.invalid"
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
for i := 0; i < r.Seuil; i++ {
|
||
|
|
r.Echec(hote, io.EOF)
|
||
|
|
}
|
||
|
|
front := &TLSFront{
|
||
|
|
CA: ca, Proxy: proxyDeTest(t), Refusals: r,
|
||
|
|
Origine: func(net.Conn) (string, error) { return coupee, nil },
|
||
|
|
}
|
||
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
t.Cleanup(func() { ln.Close() })
|
||
|
|
go front.Serve(ln)
|
||
|
|
|
||
|
|
brut, err := net.DialTimeout("tcp", ln.Addr().String(), 2*time.Second)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer brut.Close()
|
||
|
|
brut.SetDeadline(time.Now().Add(5 * time.Second))
|
||
|
|
pool := x509.NewCertPool()
|
||
|
|
pool.AddCert(ca.cert)
|
||
|
|
tc := tls.Client(brut, &tls.Config{ServerName: hote, RootCAs: pool})
|
||
|
|
if err := tc.Handshake(); err != nil {
|
||
|
|
t.Fatalf("l'amont coupé laisse le client sans poignée de main : %v", err)
|
||
|
|
}
|
||
|
|
if !r.Has(hote) {
|
||
|
|
t.Error("le repli a effacé le soupçon : il ne vaut que pour cette" +
|
||
|
|
" connexion")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une ALERTE ne se rouvre pas, quel que soit le réglage d'oubli : le client a
|
||
|
|
// REGARDÉ notre certificat. Le ré-intercepter ferait échouer de nouveau
|
||
|
|
// l'installation qui le traverse.
|
||
|
|
func TestUneAlerteNeSeRouvreJamais(t *testing.T) {
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
r.Oubli = time.Millisecond
|
||
|
|
r.Echec("epingleur.example", errRefus)
|
||
|
|
time.Sleep(10 * time.Millisecond)
|
||
|
|
if !r.Has("epingleur.example") {
|
||
|
|
t.Error("une décision du client a été oubliée")
|
||
|
|
}
|
||
|
|
if !slices.Contains(r.List(), "epingleur.example") {
|
||
|
|
t.Error("une alerte a disparu de la liste")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Le défaut n'est plus « jamais » : sans lui, une rafale condamne un hôte
|
||
|
|
// jusqu'au redémarrage du service.
|
||
|
|
func TestLOubliEstActifParDefaut(t *testing.T) {
|
||
|
|
if NewRefusals(nil).Oubli <= 0 {
|
||
|
|
t.Error("un refus de transport ne se rouvre jamais par défaut")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une réussite efface le compte : deux incidents éloignés ne doivent pas
|
||
|
|
// s'additionner jusqu'au seuil.
|
||
|
|
func TestUneReussiteEffaceLeCompte(t *testing.T) {
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
coupure := errors.New("EOF")
|
||
|
|
for i := 1; i < r.Seuil; i++ {
|
||
|
|
r.Echec("h.example", coupure)
|
||
|
|
}
|
||
|
|
r.Reussite("h.example")
|
||
|
|
for i := 1; i < r.Seuil; i++ {
|
||
|
|
r.Echec("h.example", coupure)
|
||
|
|
}
|
||
|
|
if r.Has("h.example") {
|
||
|
|
t.Error("des incidents éloignés se sont additionnés")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une ALERTE tranche tout de suite : le client a REGARDÉ notre certificat.
|
||
|
|
func TestUneAlerteCondamneDesLePremierEchec(t *testing.T) {
|
||
|
|
r := NewRefusals(nil)
|
||
|
|
if !r.Echec("epingleur.example", errors.New("remote error: tls: bad certificate")) {
|
||
|
|
t.Error("une alerte n'est pas retenue immédiatement")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un REFUS et une COUPURE ne disent pas la même chose.
|
||
|
|
//
|
||
|
|
// Le code les confondait : « connection reset by peer » était journalisé
|
||
|
|
// « certificat refusé » et faisait passer l'hôte en tunnel opaque. Un miroir
|
||
|
|
// de distribution se retrouve alors soustrait au cache sur une seule coupure,
|
||
|
|
// et tout son trafic repart à l'amont.
|
||
|
|
func TestUneCoupureNestPasUnRefus(t *testing.T) {
|
||
|
|
coupures := []error{
|
||
|
|
errors.New("read tcp 10.0.0.1:8899->10.0.0.2:33918: read:" +
|
||
|
|
" connection reset by peer"),
|
||
|
|
io.EOF,
|
||
|
|
errors.New("read tcp: i/o timeout"),
|
||
|
|
nil,
|
||
|
|
}
|
||
|
|
for _, err := range coupures {
|
||
|
|
if estRefusTLS(err) {
|
||
|
|
t.Errorf("%v pris pour un refus du client", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUneAlerteEstUnRefus(t *testing.T) {
|
||
|
|
refus := []error{
|
||
|
|
errors.New("remote error: tls: bad certificate"),
|
||
|
|
errors.New("remote error: tls: unknown certificate authority"),
|
||
|
|
tls.AlertError(42),
|
||
|
|
}
|
||
|
|
for _, err := range refus {
|
||
|
|
if !estRefusTLS(err) {
|
||
|
|
t.Errorf("%v n'est pas reconnu comme un refus", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// L'erreur enveloppée compte autant : la bibliothèque en emballe parfois.
|
||
|
|
func TestUnRefusEnveloppeEstReconnu(t *testing.T) {
|
||
|
|
err := fmt.Errorf("poignée de main : %w", tls.AlertError(48))
|
||
|
|
if !estRefusTLS(err) {
|
||
|
|
t.Error("un refus enveloppé n'est pas reconnu")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// clientHelloBrut fabrique un ClientHello valide portant ce nom, sans rien
|
||
|
|
// engager : les octets sont capturés sur un tuyau, puis rejoués à la main.
|
||
|
|
func clientHelloBrut(t *testing.T, nom string) []byte {
|
||
|
|
t.Helper()
|
||
|
|
a, b := net.Pipe()
|
||
|
|
defer a.Close()
|
||
|
|
go func() {
|
||
|
|
tc := tls.Client(a, &tls.Config{ServerName: nom})
|
||
|
|
_ = tc.Handshake()
|
||
|
|
}()
|
||
|
|
b.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||
|
|
brut, err := readFirstRecord(b)
|
||
|
|
b.Close()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ClientHello : %v", err)
|
||
|
|
}
|
||
|
|
return brut
|
||
|
|
}
|
||
|
|
|
||
|
|
// Le contrôle précédent vérifie la RÈGLE ; celui-ci vérifie qu'elle est
|
||
|
|
// branchée. Sans lui, remettre les deux cas dans le même sac laisse les tests
|
||
|
|
// verts et recondamne un miroir au tunnel dès la première coupure.
|
||
|
|
func TestUneCoupureNeCondamnePasLHote(t *testing.T) {
|
||
|
|
ca, err := LoadOrCreateCA(t.TempDir())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
refus := NewRefusals(nil)
|
||
|
|
front := &TLSFront{CA: ca, Refusals: refus, Proxy: proxyDeTest(t)}
|
||
|
|
hello := clientHelloBrut(t, "miroir.example")
|
||
|
|
|
||
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer ln.Close()
|
||
|
|
|
||
|
|
fini := make(chan struct{})
|
||
|
|
go func() {
|
||
|
|
if c, err := ln.Accept(); err == nil {
|
||
|
|
front.handle(c)
|
||
|
|
}
|
||
|
|
close(fini)
|
||
|
|
}()
|
||
|
|
|
||
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// Le ClientHello part, puis la connexion est coupée BRUTALEMENT : c'est
|
||
|
|
// la coupure de transport que le code prenait pour un rejet d'autorité.
|
||
|
|
// Aucune alerte n'est émise — ce serait l'autre cas.
|
||
|
|
if _, err := c.Write(hello); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
c.(*net.TCPConn).SetLinger(0)
|
||
|
|
c.Close()
|
||
|
|
|
||
|
|
select {
|
||
|
|
case <-fini:
|
||
|
|
case <-time.After(3 * time.Second):
|
||
|
|
t.Fatal("la connexion n'a pas été traitée")
|
||
|
|
}
|
||
|
|
if refus.Has("miroir.example") {
|
||
|
|
t.Error("une coupure a condamné l'hôte au tunnel opaque")
|
||
|
|
}
|
||
|
|
}
|