Hej Habr, jag heter Ilya och jobbar i plattformsteamet på Exness. Vi utvecklar och implementerar grundläggande infrastrukturkomponenter som våra produktutvecklingsteam använder.
I den här artikeln vill jag dela med mig av mina erfarenheter av att implementera krypterad SNI-teknik (ESNI) i infrastrukturen för offentliga webbplatser.

Användningen av denna teknik kommer att öka säkerhetsnivån vid arbete med en offentlig webbplats och följa de interna säkerhetsstandarder som antagits av företaget.
Först och främst vill jag påpeka att tekniken inte är standardiserad och fortfarande är i utkastfasen, men CloudFlare och Mozilla stöder den redan (i ). Det är detta som motiverade oss att genomföra ett sådant experiment.
Lite teori
ESNI – är en utökning av TLS 1.3-protokollet som låter dig kryptera SNI i TLS-handskakningsmeddelandet "Client Hello". Så här ser ett Client Hello med ESNI-stöd ut (istället för det vanliga SNI ser vi ESNI):

För att använda ESNI krävs tre komponenter:
- DNS;
- Kundsupport;
- Serversidesupport.
DNS
Du behöver lägga till två DNS-poster – AOch TXT (TXT-posten innehåller den publika nyckeln som klienten kan kryptera SNI med) – se nedan. Dessutom bör det finnas stöd för DoH (DNS över HTTPS), eftersom de tillgängliga klienterna (se nedan) inte aktiverar ESNI-stöd utan DoH. Detta är logiskt, eftersom ESNI innebär kryptering av resursnamnet vi åtkommer, d.v.s. det är meningslöst att komma åt DNS via UDP. Dessutom, att använda låter dig skydda mot cache-förgiftningsattacker i det här scenariot.
För närvarande tillgänglig , bland dem:
CloudFlare (Kontrollera min webbläsare → Krypterad SNI → Läs mer), att deras servrar redan stöder ESNI, det vill säga att för CloudFlare-servrar i DNS har vi minst två poster - A och TXT. I exemplet nedan begär vi Google DNS (via HTTPS):
А inträde:
curl 'https://dns.google.com/resolve?name=www.cloudflare.com&type=A'
-s -H 'accept: application/dns+json'
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": true,
"CD": false,
"Question": [
{
"name": "www.cloudflare.com.",
"type": 1
}
],
"Answer": [
{
"name": "www.cloudflare.com.",
"type": 1,
"TTL": 257,
"data": "104.17.210.9"
},
{
"name": "www.cloudflare.com.",
"type": 1,
"TTL": 257,
"data": "104.17.209.9"
}
]
}
TXT post, begäran genereras enligt en mall _esni.FQDN:
curl 'https://dns.google.com/resolve?name=_esni.www.cloudflare.com&type=TXT'
-s -H 'accept: application/dns+json'
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": true,
"CD": false,
"Question": [
{
"name": "_esni.www.cloudflare.com.",
"type": 16
}
],
"Answer": [
{
"name": "_esni.www.cloudflare.com.",
"type": 16,
"TTL": 1799,
"data": ""/wEUgUKlACQAHQAg9SiAYQ9aUseUZr47HYHvF5jkt3aZ5802eAMJPhRz1QgAAhMBAQQAAAAAXtUmAAAAAABe3Q8AAAA=""
}
],
"Comment": "Response from 2400:cb00:2049:1::a29f:209."
}
Så, ur ett DNS-perspektiv, bör vi använda DoH (helst med DNSSEC) och lägga till två poster.
Kundsupport
Om vi pratar om webbläsare, så för närvarande . Instruktioner om hur man aktiverar stöd för ESNI och DoH i FireFox finns. När webbläsaren har konfigurerats bör vi se något liknande:

för att kontrollera webbläsaren.
Självklart måste TLS 1.3 användas för att stödja ESNI, eftersom ESNI är en utökning av TLS 1.3.
För att testa backend-systemet med ESNI-stöd implementerade vi en klient på go, Men mer om det senare.
Serversidesupport
För närvarande stöds inte ESNI av webbservrar som nginx/apache etc., eftersom de arbetar med TLS via OpenSSL/BoringSSL, vilka inte officiellt stöder ESNI.
Därför bestämde vi oss för att skapa vår egen frontend-komponent (ESNI reverse proxy), som skulle stödja TLS 1.3-terminering med ESNI och proxya HTTP(S)-trafik till uppströms, vilket inte stöder ESNI. Detta möjliggör användning av tekniken i en redan befintlig infrastruktur, utan att ändra huvudkomponenterna – det vill säga att använda befintliga webbservrar som inte stöder ESNI.
För tydlighetens skull, här är ett diagram:

Jag vill påpeka att proxyn utformades med möjligheten att avsluta en TLS-anslutning utan ESNI, för att stödja klienter utan ESNI. Dessutom kan protokollet för kommunikation med uppströmsleverantören vara antingen HTTP eller HTTPS med en TLS-version under 1.3 (om uppströmsleverantören inte stöder 1.3). Detta schema ger maximal flexibilitet.
Implementering av ESNI-stöd på go vi lånade från Jag vill omedelbart notera att själva implementeringen är ganska icke-trivial, eftersom den innebär förändringar i standardbiblioteket. krypto/tls och kräver därför "patchning" GOROOT före montering.
För att generera ESNI-nycklar använde vi (också en CloudFlare-skapelse). Dessa nycklar används för att kryptera/dekryptera SNI.
Vi testade bygget med go 1.13 på Linux (Debian, Alpine) och MacOS.
Några ord om operativa funktioner
ESNI:s omvända proxy tillhandahåller mätvärden i Prometheus-format, såsom rps, uppströmslatens och svarskoder, misslyckade/lyckade TLS-handskakningar och TLS-handskakningens varaktighet. Vid första anblicken verkade detta tillräckligt för att bedöma hur proxyn hanterar trafik.
Vi utförde även belastningstester före användning. Resultaten är nedan:
wrk -t50 -c1000 -d360s 'https://esni-rev-proxy.npw:443' --timeout 15s
Running 6m test @ https://esni-rev-proxy.npw:443
50 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.77s 1.21s 7.20s 65.43%
Req/Sec 13.78 8.84 140.00 83.70%
206357 requests in 6.00m, 6.08GB read
Requests/sec: 573.07
Transfer/sec: 17.28MB
Vi utförde rent kvalitativa belastningstester för att jämföra schemat med och utan ESNI reverse proxy. Vi "hällde" trafik lokalt för att eliminera "störningar" i mellanliggande komponenter.
Så, med ESNI-stöd och uppströmsproxy med HTTP, fick vi runt ~550 rps från en instans, med den genomsnittliga CPU/RAM-förbrukningen för ESNI omvänd proxy:
- 80 % CPU-användning (4 vCPU, 4 GB RAM-värdar, Linux)
- 130 MB minnes-RSS

Som jämförelse är RPS för samma nginx uppströms utan TLS-terminering (HTTP-protokoll) ~1100:
wrk -t50 -c1000 -d360s 'http://lb.npw:80' –-timeout 15s
Running 6m test @ http://lb.npw:80
50 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.11s 2.30s 15.00s 90.94%
Req/Sec 23.25 13.55 282.00 79.25%
393093 requests in 6.00m, 11.35GB read
Socket errors: connect 0, read 0, write 0, timeout 9555
Non-2xx or 3xx responses: 8111
Requests/sec: 1091.62
Transfer/sec: 32.27MB
Förekomsten av timeouts indikerar att det saknas resurser (vi använde 4 vCPU, 4 GB RAM-värdar, Linux), och faktum är att den potentiella RPS är högre (vi fick siffror på upp till 2700 RPS på kraftfullare resurser).
Sammanfattningsvis vill jag påpeka att att ESNI-tekniken ser ganska lovande ut. Det finns fortfarande många öppna frågor, till exempel frågorna om att lagra den publika ESNI-nyckeln i DNS och rotera ESNI-nycklar – dessa frågor diskuteras aktivt, och den senaste versionen av utkastet (i skrivande stund) av ESNI finns redan .
Källa: will.com
