Skip to content

Commit f2ea186

Browse files
committed
Add cert probe caching and configurable probe count
Cache probe results to JSON file to avoid 15 TLS connections on every restart. Add noise_cache_path, noise_cache_ttl, and noise_probe_count config options under [defense.doppelganger].
1 parent bc55e8e commit f2ea186

File tree

5 files changed

+121
-14
lines changed

5 files changed

+121
-14
lines changed

internal/cli/run_proxy.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,10 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
269269
DoppelGangerDRS: conf.Defense.Doppelganger.DRS.Get(false),
270270
DoppelGangerIdlePadding: conf.Defense.Doppelganger.IdlePadding.Get(false),
271271

272+
NoiseProbeCount: conf.Defense.Doppelganger.NoiseProbeCount.Get(0),
273+
NoiseCacheTTL: conf.Defense.Doppelganger.NoiseCacheTTL.Get(0),
274+
NoiseCachePath: conf.Defense.Doppelganger.NoiseCachePath,
275+
272276
APIBindTo: conf.APIBindTo.Get(""),
273277
}
274278

internal/config/config.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,14 @@ type Config struct {
5151
Blocklist ListConfig `json:"blocklist"`
5252
Allowlist ListConfig `json:"allowlist"`
5353
Doppelganger struct {
54-
URLs []TypeHttpsURL `json:"urls"`
55-
Repeats TypeConcurrency `json:"repeats_per_raid"`
56-
UpdateEach TypeDuration `json:"raid_each"`
57-
DRS TypeBool `json:"drs"`
58-
IdlePadding TypeBool `json:"idle_padding"`
54+
URLs []TypeHttpsURL `json:"urls"`
55+
Repeats TypeConcurrency `json:"repeats_per_raid"`
56+
UpdateEach TypeDuration `json:"raid_each"`
57+
DRS TypeBool `json:"drs"`
58+
IdlePadding TypeBool `json:"idle_padding"`
59+
NoiseProbeCount TypeConcurrency `json:"noise_probe_count"`
60+
NoiseCacheTTL TypeDuration `json:"noise_cache_ttl"`
61+
NoiseCachePath string `json:"noise_cache_path"`
5962
} `json:"doppelganger"`
6063
} `json:"defense"`
6164
Network struct {

mtglib/internal/tls/fake/cert_probe.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package fake
33
import (
44
"crypto/tls"
55
"encoding/binary"
6+
"encoding/json"
67
"fmt"
78
"net"
9+
"os"
810
"sync"
911
"time"
1012
)
@@ -13,15 +15,76 @@ const (
1315
probeDialTimeout = 10 * time.Second
1416
probeHandshakeTimeout = 10 * time.Second
1517
defaultProbeCount = 15
18+
defaultCacheTTL = 24 * time.Hour
1619

1720
tlsTypeChangeCipherSpec = 0x14
1821
tlsTypeApplicationData = 0x17
1922
)
2023

2124
// CertProbeResult holds the measured encrypted handshake size.
2225
type CertProbeResult struct {
23-
Mean int
24-
Jitter int
26+
Mean int `json:"mean"`
27+
Jitter int `json:"jitter"`
28+
}
29+
30+
// CertProbeCache is the on-disk format for cached probe results.
31+
type CertProbeCache struct {
32+
Hostname string `json:"hostname"`
33+
Port int `json:"port"`
34+
Mean int `json:"mean"`
35+
Jitter int `json:"jitter"`
36+
ProbedAt time.Time `json:"probed_at"`
37+
}
38+
39+
// LoadCachedProbe reads a cached probe result from path. Returns the result
40+
// and true if the cache exists, matches hostname:port, and is younger than ttl.
41+
// Otherwise returns zero value and false.
42+
func LoadCachedProbe(path, hostname string, port int, ttl time.Duration) (CertProbeResult, bool) {
43+
if ttl <= 0 {
44+
ttl = defaultCacheTTL
45+
}
46+
47+
data, err := os.ReadFile(path)
48+
if err != nil {
49+
return CertProbeResult{}, false
50+
}
51+
52+
var cache CertProbeCache
53+
if err := json.Unmarshal(data, &cache); err != nil {
54+
return CertProbeResult{}, false
55+
}
56+
57+
if cache.Hostname != hostname || cache.Port != port {
58+
return CertProbeResult{}, false
59+
}
60+
61+
if time.Since(cache.ProbedAt) > ttl {
62+
return CertProbeResult{}, false
63+
}
64+
65+
if cache.Mean <= 0 {
66+
return CertProbeResult{}, false
67+
}
68+
69+
return CertProbeResult{Mean: cache.Mean, Jitter: cache.Jitter}, true
70+
}
71+
72+
// SaveCachedProbe writes a probe result to path as JSON.
73+
func SaveCachedProbe(path, hostname string, port int, result CertProbeResult) error {
74+
cache := CertProbeCache{
75+
Hostname: hostname,
76+
Port: port,
77+
Mean: result.Mean,
78+
Jitter: result.Jitter,
79+
ProbedAt: time.Now(),
80+
}
81+
82+
data, err := json.MarshalIndent(cache, "", " ")
83+
if err != nil {
84+
return err
85+
}
86+
87+
return os.WriteFile(path, data, 0o644) //nolint: gosec
2588
}
2689

2790
// ProbeCertSize connects to hostname:port via TLS multiple times and measures

mtglib/proxy.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -392,13 +392,38 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
392392
probePort := opts.getDomainFrontingPort()
393393
noiseParams := fake.NoiseParams{}
394394

395-
probeResult, err := fake.ProbeCertSize(probeHost, probePort, 15)
396-
if err != nil {
397-
logger.WarningError("cert probe failed, using default noise size", err)
398-
} else {
399-
noiseParams = fake.NoiseParams(probeResult)
400-
logger.Info(fmt.Sprintf("cert probe: host=%s mean=%d jitter=%d",
401-
probeHost, probeResult.Mean, probeResult.Jitter))
395+
probeCount := int(opts.NoiseProbeCount)
396+
if probeCount <= 0 {
397+
probeCount = 15
398+
}
399+
400+
cacheTTL := opts.NoiseCacheTTL
401+
402+
// Try loading from cache first.
403+
if opts.NoiseCachePath != "" {
404+
if cached, ok := fake.LoadCachedProbe(opts.NoiseCachePath, probeHost, probePort, cacheTTL); ok {
405+
noiseParams = fake.NoiseParams(cached)
406+
logger.Info(fmt.Sprintf("cert probe: loaded from cache, host=%s mean=%d jitter=%d",
407+
probeHost, cached.Mean, cached.Jitter))
408+
}
409+
}
410+
411+
// If no cached result, probe live.
412+
if noiseParams.Mean == 0 {
413+
probeResult, err := fake.ProbeCertSize(probeHost, probePort, probeCount)
414+
if err != nil {
415+
logger.WarningError("cert probe failed, using default noise size", err)
416+
} else {
417+
noiseParams = fake.NoiseParams(probeResult)
418+
logger.Info(fmt.Sprintf("cert probe: host=%s mean=%d jitter=%d",
419+
probeHost, probeResult.Mean, probeResult.Jitter))
420+
421+
if opts.NoiseCachePath != "" {
422+
if saveErr := fake.SaveCachedProbe(opts.NoiseCachePath, probeHost, probePort, probeResult); saveErr != nil {
423+
logger.WarningError("failed to save cert probe cache", saveErr)
424+
}
425+
}
426+
}
402427
}
403428

404429
proxy := &Proxy{

mtglib/proxy_opts.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ type ProxyOpts struct {
173173
// during idle periods to mimic HTTP/2 PING keepalive traffic.
174174
DoppelGangerIdlePadding bool
175175

176+
// NoiseProbeCount is the number of TLS connections to make when probing
177+
// the fronting domain's cert chain size. Default is 15.
178+
NoiseProbeCount uint
179+
180+
// NoiseCacheTTL is how long a cached cert probe result is considered valid.
181+
// Default is 24 hours.
182+
NoiseCacheTTL time.Duration
183+
184+
// NoiseCachePath is the file path for caching cert probe results between
185+
// restarts. If empty, no caching is performed.
186+
NoiseCachePath string
187+
176188
// APIBindTo is the address to bind the stats HTTP API server to.
177189
// If empty, the stats API server is not started.
178190
//

0 commit comments

Comments
 (0)