feat(everything): fix doh and increase min TLS to 1.3

This commit is contained in:
2026-07-08 08:42:39 +01:00
parent 0c03bcbcfe
commit cc5e0ab00a
13 changed files with 691 additions and 530 deletions
+1
View File
@@ -18,3 +18,4 @@
/results /results
/results.bak /results.bak
/results_merged /results_merged
dns_results*
+10
View File
@@ -0,0 +1,10 @@
.PHONY: all clean
all:
python3 scripts/post_processing/merge_files.py
python3 scripts/post_processing/merge_cpu.py
python3 scripts/post_processing/merge_mem.py
python3 scripts/post_processing/merge_pcap.py
clean:
rm -f ./dns_results.csv ./dns_results_cpu.csv ./dns_results_mem.csv ./dns_results_pcap.csv
+58 -18
View File
@@ -27,7 +27,6 @@ type Config struct {
Path string Path string
DNSSEC bool DNSSEC bool
HTTP3 bool HTTP3 bool
HTTP2 bool
KeepAlive bool KeepAlive bool
} }
@@ -62,6 +61,7 @@ func New(config Config) (*Client, error) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ServerName: config.Host, ServerName: config.Host,
MinVersion: tls.VersionTLS13,
ClientSessionCache: tls.NewLRUClientSessionCache(100), ClientSessionCache: tls.NewLRUClientSessionCache(100),
} }
@@ -88,14 +88,7 @@ func New(config Config) (*Client, error) {
} }
var transportType string var transportType string
if config.HTTP2 { if config.HTTP3 {
http2Transport := &http2.Transport{
TLSClientConfig: tlsConfig,
AllowHTTP: true,
}
httpClient.Transport = http2Transport
transportType = "HTTP/2"
} else if config.HTTP3 {
quicTlsConfig := http3.ConfigureTLSConfig(tlsConfig) quicTlsConfig := http3.ConfigureTLSConfig(tlsConfig)
http3Transport := &http3.Transport{ http3Transport := &http3.Transport{
TLSClientConfig: quicTlsConfig, TLSClientConfig: quicTlsConfig,
@@ -104,7 +97,12 @@ func New(config Config) (*Client, error) {
httpClient.Transport = http3Transport httpClient.Transport = http3Transport
transportType = "HTTP/3" transportType = "HTTP/3"
} else { } else {
transportType = "HTTP/1.1" http2Transport := &http2.Transport{
TLSClientConfig: tlsConfig,
AllowHTTP: true,
}
httpClient.Transport = http2Transport
transportType = "HTTP/2"
} }
logger.Debug("DoH client created: %s (%s, DNSSEC: %v, KeepAlive: %v)", rawURL, transportType, config.DNSSEC, config.KeepAlive) logger.Debug("DoH client created: %s (%s, DNSSEC: %v, KeepAlive: %v)", rawURL, transportType, config.DNSSEC, config.KeepAlive)
@@ -116,6 +114,43 @@ func New(config Config) (*Client, error) {
}, nil }, nil
} }
// newSingleUseClient builds a throwaway *http.Client whose transport is torn
// down by the returned closeFn. Used for non-persistent (non-keep-alive) mode
// so that each query establishes and tears down its own connection, and thus
// pays its own TCP+TLS (or QUIC) handshake. Without this, the pooled
// HTTP/2 transport silently reuses a single connection across all queries
// (HTTP/2 multiplexes and ignores the Connection: close header), which would
// make the non-persistent measurement indistinguishable from persistent.
func (c *Client) newSingleUseClient() (*http.Client, func()) {
tlsConfig := &tls.Config{
ServerName: c.config.Host,
MinVersion: tls.VersionTLS13,
ClientSessionCache: nil, // no resumption: force a full handshake each query
}
if c.config.HTTP3 {
quicConf := &quic.Config{
MaxIdleTimeout: 30 * time.Second,
DisablePathMTUDiscovery: true,
}
quicTLS := http3.ConfigureTLSConfig(tlsConfig)
tr := &http3.Transport{
TLSClientConfig: quicTLS,
QUICConfig: quicConf,
}
client := &http.Client{Transport: tr, Timeout: 30 * time.Second}
return client, func() { tr.Close() }
}
tr := &http2.Transport{
TLSClientConfig: tlsConfig,
AllowHTTP: true,
DisableCompression: true,
}
client := &http.Client{Transport: tr, Timeout: 30 * time.Second}
return client, func() { tr.CloseIdleConnections() }
}
func (c *Client) Close() { func (c *Client) Close() {
logger.Debug("Closing DoH client") logger.Debug("Closing DoH client")
if t, ok := c.httpClient.Transport.(*http.Transport); ok { if t, ok := c.httpClient.Transport.(*http.Transport); ok {
@@ -143,6 +178,18 @@ func (c *Client) Query(msg *dns.Msg) (*dns.Msg, error) {
return nil, fmt.Errorf("doh: failed to pack DNS message: %w", err) return nil, fmt.Errorf("doh: failed to pack DNS message: %w", err)
} }
// Select the HTTP client. In persistent mode, reuse the pooled client built
// in New(). In non-persistent mode, build a fresh client (and transport)
// per query and tear it down afterwards, so every query pays its own
// handshake — otherwise HTTP/2 would reuse one connection and the
// non-persistent measurement would be wrong.
httpClient := c.httpClient
if !c.config.KeepAlive {
freshClient, closeFn := c.newSingleUseClient()
defer closeFn()
httpClient = freshClient
}
httpReq, err := http.NewRequest(http.MethodPost, c.upstreamURL.String(), bytes.NewReader(packedMsg)) httpReq, err := http.NewRequest(http.MethodPost, c.upstreamURL.String(), bytes.NewReader(packedMsg))
if err != nil { if err != nil {
logger.Error("DoH failed to create HTTP request: %v", err) logger.Error("DoH failed to create HTTP request: %v", err)
@@ -153,14 +200,7 @@ func (c *Client) Query(msg *dns.Msg) (*dns.Msg, error) {
httpReq.Header.Set("Content-Type", dnsMessageContentType) httpReq.Header.Set("Content-Type", dnsMessageContentType)
httpReq.Header.Set("Accept", dnsMessageContentType) httpReq.Header.Set("Accept", dnsMessageContentType)
// Set Connection header based on KeepAlive setting httpResp, err := httpClient.Do(httpReq)
if c.config.KeepAlive {
httpReq.Header.Set("Connection", "keep-alive")
} else {
httpReq.Header.Set("Connection", "close")
}
httpResp, err := c.httpClient.Do(httpReq)
if err != nil { if err != nil {
logger.Error("DoH request failed to %s: %v", c.upstreamURL.Host, err) logger.Error("DoH request failed to %s: %v", c.upstreamURL.Host, err)
return nil, fmt.Errorf("doh: failed executing HTTP request to %s: %w", c.upstreamURL.Host, err) return nil, fmt.Errorf("doh: failed executing HTTP request to %s: %w", c.upstreamURL.Host, err)
+2 -2
View File
@@ -26,7 +26,7 @@ type Client struct {
targetAddr *net.UDPAddr targetAddr *net.UDPAddr
tlsConfig *tls.Config tlsConfig *tls.Config
udpConn *net.UDPConn udpConn *net.UDPConn
quicConn quic.Connection quicConn *quic.Conn
quicTransport *quic.Transport quicTransport *quic.Transport
quicConfig *quic.Config quicConfig *quic.Config
config Config config Config
@@ -119,7 +119,7 @@ func (c *Client) Query(msg *dns.Msg) (*dns.Msg, error) {
return nil, fmt.Errorf("doq: failed to pack message: %w", err) return nil, fmt.Errorf("doq: failed to pack message: %w", err)
} }
var quicStream quic.Stream var quicStream *quic.Stream
quicStream, err = c.quicConn.OpenStream() quicStream, err = c.quicConn.OpenStream()
if err != nil { if err != nil {
logger.Debug("DoQ stream failed, reconnecting: %v", err) logger.Debug("DoQ stream failed, reconnecting: %v", err)
+1
View File
@@ -49,6 +49,7 @@ func New(config Config) (*Client, error) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ServerName: config.Host, ServerName: config.Host,
MinVersion: tls.VersionTLS13,
} }
client := &Client{ client := &Client{
+14 -18
View File
@@ -1,29 +1,25 @@
module github.com/afonsofrancof/sdns-proxy module github.com/afonsofrancof/sdns-proxy
go 1.24.1 go 1.26.4
require ( require (
github.com/alecthomas/kong v1.8.1 github.com/alecthomas/kong v1.15.0
github.com/ameshkov/dnscrypt/v2 v2.4.0 github.com/ameshkov/dnscrypt/v2 v2.4.0
github.com/google/gopacket v1.1.19 github.com/google/gopacket v1.1.19
github.com/miekg/dns v1.1.65 github.com/miekg/dns v1.1.72
github.com/quic-go/quic-go v0.50.0 github.com/quic-go/quic-go v0.60.0
golang.org/x/net v0.38.0 golang.org/x/net v0.56.0
) )
require ( require (
github.com/AdguardTeam/golibs v0.32.7 // indirect github.com/AdguardTeam/golibs v0.35.14 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect golang.org/x/crypto v0.53.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
github.com/quic-go/qpack v0.5.1 // indirect golang.org/x/mod v0.37.0 // indirect
go.uber.org/mock v0.5.0 // indirect golang.org/x/sync v0.21.0 // indirect
golang.org/x/crypto v0.37.0 // indirect golang.org/x/sys v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/text v0.39.0 // indirect
golang.org/x/mod v0.24.0 // indirect golang.org/x/tools v0.47.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.31.0 // indirect
) )
+34 -58
View File
@@ -1,88 +1,64 @@
github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4= github.com/AdguardTeam/golibs v0.35.14 h1:LGqoRMbuxUgZyhOjlkmPxy5t1/XPuFpDcG5RBIDcskA=
github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA= github.com/AdguardTeam/golibs v0.35.14/go.mod h1:FL470ALCuUs7cwk88ltBgTZ6JYCUG2NpNzp3EOMirxo=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE= github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI=
github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o= github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o=
github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI= github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -1,405 +1,258 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Fast PCAP Preprocessor for DNS QoS Analysis Fast PCAP Preprocessor for DNS QoS Analysis.
Loads PCAP into memory first, then uses binary search for matching.
Uses LAN IP to determine direction (LAN = sent, non-LAN = received). Loads PCAP into memory, then uses binary search to match packets to query
windows. Direction is determined by a configured local IP (the netns veth IP).
""" """
import argparse
import bisect
import csv import csv
import shutil import shutil
import socket
import time
from pathlib import Path from pathlib import Path
from typing import Dict, List, NamedTuple from typing import Dict, List, NamedTuple
import time
import dpkt import dpkt
from dateutil import parser as date_parser from dateutil import parser as date_parser
BANDWIDTH_COLUMNS = [ BANDWIDTH_COLUMNS = [
'bytes_sent', "bytes_sent",
'bytes_received', "bytes_received",
'packets_sent', "packets_sent",
'packets_received', "packets_received",
'total_bytes', "total_bytes",
] ]
DEFAULT_LOCAL_IP = "192.168.100.2" # netns veth1 address
DEFAULT_PROVIDERS = ["adguard", "cloudflare", "google", "quad9"]
class Packet(NamedTuple): class Packet(NamedTuple):
"""Lightweight packet representation."""
timestamp: float timestamp: float
size: int size: int
is_outbound: bool # True if from LAN, False if from internet is_outbound: bool
class QueryWindow: def needs_processing(csv_path: Path) -> bool:
"""Efficient query window representation.""" """True if file lacks bandwidth columns OR all values are empty/zero."""
__slots__ = ['index', 'start', 'end', 'sent', 'received', 'pkts_sent', 'pkts_received']
def __init__(self, index: int, start: float, end: float):
self.index = index
self.start = start
self.end = end
self.sent = 0
self.received = 0
self.pkts_sent = 0
self.pkts_received = 0
def is_already_processed(csv_path: Path) -> bool:
"""
Check if CSV has already been processed.
Returns True if bandwidth columns exist AND at least one row has non-zero data.
"""
try: try:
with open(csv_path, 'r', encoding='utf-8') as f: with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
# Check if columns exist
if not reader.fieldnames: if not reader.fieldnames:
return False return True
if not all(c in reader.fieldnames for c in BANDWIDTH_COLUMNS):
if not all(col in reader.fieldnames for col in BANDWIDTH_COLUMNS): return True
return False
# Check if any row has non-zero bandwidth data
for row in reader: for row in reader:
for col in BANDWIDTH_COLUMNS: for col in BANDWIDTH_COLUMNS:
val = row.get(col, '').strip() val = (row.get(col) or "").strip()
if val and val != '0': if val and val != "0":
return True return False # has real data
return True # columns exist but all empty/zero
# All rows have zero/empty values - not truly processed
return False
except Exception: except Exception:
return False return True
def parse_csv_timestamp(ts_str: str) -> float: def parse_csv_timestamp(ts_str: str) -> float:
"""Convert RFC3339Nano timestamp to Unix epoch (seconds).""" return date_parser.isoparse(ts_str).timestamp()
dt = date_parser.isoparse(ts_str)
return dt.timestamp()
def is_lan_ip(ip_bytes: bytes) -> bool: def load_pcap(pcap_path: Path, local_ip_bytes: bytes) -> List[Packet]:
"""Check if IP is a private/LAN address.""" """Load PCAP into a list of Packets sorted by timestamp."""
if len(ip_bytes) != 4: print(" Loading PCAP...")
return False t0 = time.time()
packets: List[Packet] = []
first = ip_bytes[0]
second = ip_bytes[1]
# 10.0.0.0/8
if first == 10:
return True
# 172.16.0.0/12
if first == 172 and 16 <= second <= 31:
return True
# 192.168.0.0/16
if first == 192 and second == 168:
return True
# 127.0.0.0/8 (localhost)
if first == 127:
return True
return False
def load_pcap_into_memory(pcap_path: Path) -> List[Packet]:
"""Load all packets from PCAP into memory with minimal data."""
packets = []
print(f" Loading PCAP into memory...")
start_time = time.time()
with open(pcap_path, "rb") as f:
try: try:
with open(pcap_path, 'rb') as f: reader = dpkt.pcap.Reader(f)
try: except ValueError:
pcap = dpkt.pcap.Reader(f)
except:
# Try pcapng format
f.seek(0) f.seek(0)
pcap = dpkt.pcapng.Reader(f) reader = dpkt.pcapng.Reader(f)
for ts, buf in pcap: for ts, buf in reader:
try: try:
packet_time = float(ts)
packet_size = len(buf)
# Parse to get source IP
eth = dpkt.ethernet.Ethernet(buf) eth = dpkt.ethernet.Ethernet(buf)
# Default to outbound if we can't determine
is_outbound = True
if isinstance(eth.data, dpkt.ip.IP):
ip = eth.data ip = eth.data
src_ip = ip.src if not isinstance(ip, dpkt.ip.IP):
is_outbound = is_lan_ip(src_ip) continue
if ip.src == local_ip_bytes:
packets.append(Packet( is_outbound = True
timestamp=packet_time, elif ip.dst == local_ip_bytes:
size=packet_size, is_outbound = False
is_outbound=is_outbound else:
)) continue # not our traffic
packets.append(Packet(float(ts), len(buf), is_outbound))
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError, AttributeError): except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError, AttributeError):
continue continue
except Exception as e:
print(f" Error reading PCAP: {e}")
return []
elapsed = time.time() - start_time
print(f" Loaded {len(packets):,} packets in {elapsed:.2f}s")
# Sort by timestamp for binary search
packets.sort(key=lambda p: p.timestamp) packets.sort(key=lambda p: p.timestamp)
print(f" Loaded {len(packets):,} packets in {time.time() - t0:.2f}s")
return packets return packets
def find_packets_in_window(
packets: List[Packet],
start_time: float,
end_time: float,
left_hint: int = 0
) -> tuple[List[Packet], int]:
"""
Binary search to find all packets within time window.
Returns (matching_packets, left_index_hint_for_next_search).
"""
if not packets:
return [], 0
# Binary search for first packet >= start_time
left, right = left_hint, len(packets) - 1
first_idx = len(packets)
while left <= right:
mid = (left + right) // 2
if packets[mid].timestamp >= start_time:
first_idx = mid
right = mid - 1
else:
left = mid + 1
# No packets in range
if first_idx >= len(packets) or packets[first_idx].timestamp > end_time:
return [], first_idx
# Collect all packets in window
matching = []
idx = first_idx
while idx < len(packets) and packets[idx].timestamp <= end_time:
matching.append(packets[idx])
idx += 1
return matching, first_idx
def load_csv_queries(csv_path: Path) -> List[Dict]: def load_csv_queries(csv_path: Path) -> List[Dict]:
"""Load CSV and create query data structures."""
queries = [] queries = []
with open(csv_path, 'r', encoding='utf-8') as f: with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.DictReader(f) for row in csv.DictReader(f):
for row in reader:
try: try:
ts_epoch = parse_csv_timestamp(row['timestamp']) start = parse_csv_timestamp(row["timestamp"])
duration_s = float(row['duration_ns']) / 1e9 duration = float(row["duration_ns"]) / 1e9
queries.append({ queries.append(
'data': row, {"data": row, "start_time": start, "end_time": start + duration}
'start_time': ts_epoch,
'end_time': ts_epoch + duration_s,
})
except Exception as e:
print(f" Warning: Skipping row - {e}")
continue
return queries
def match_packets_to_queries(
packets: List[Packet],
queries: List[Dict]
) -> List[Dict]:
"""Match packets to query windows using binary search."""
if not queries or not packets:
return queries
print(f" Matching packets to queries...")
start_time = time.time()
# Initialize metrics
for q in queries:
q['bytes_sent'] = 0
q['bytes_received'] = 0
q['packets_sent'] = 0
q['packets_received'] = 0
q['total_bytes'] = 0
# Sort queries by start time for sequential processing
queries_sorted = sorted(enumerate(queries), key=lambda x: x[1]['start_time'])
matched_packets = 0
left_hint = 0 # Optimization: start next search from here
for original_idx, q in queries_sorted:
matching, left_hint = find_packets_in_window(
packets,
q['start_time'],
q['end_time'],
left_hint
) )
except Exception as e:
for pkt in matching: print(f" Warning: skipping row - {e}")
matched_packets += 1
if pkt.is_outbound:
q['bytes_sent'] += pkt.size
q['packets_sent'] += 1
else:
q['bytes_received'] += pkt.size
q['packets_received'] += 1
q['total_bytes'] = q['bytes_sent'] + q['bytes_received']
elapsed = time.time() - start_time
print(f" Matched {matched_packets:,} packets in {elapsed:.2f}s")
# Statistics
total_sent = sum(q['bytes_sent'] for q in queries)
total_recv = sum(q['bytes_received'] for q in queries)
queries_with_data = sum(1 for q in queries if q['total_bytes'] > 0)
print(f" Total: {total_sent:,} bytes sent, {total_recv:,} bytes received")
print(f" Queries with data: {queries_with_data}/{len(queries)}")
return queries return queries
def write_enriched_csv( def match_packets(packets: List[Packet], queries: List[Dict]) -> int:
csv_path: Path, queries: List[Dict], backup: bool = True """Assign bandwidth metrics to each query. Returns total matched packets."""
): if not packets or not queries:
"""Write enriched CSV with bandwidth columns.""" for q in queries:
if backup and csv_path.exists(): q.update({c: 0 for c in BANDWIDTH_COLUMNS})
backup_path = csv_path.with_suffix('.csv.bak') return 0
if not backup_path.exists(): # Don't overwrite existing backup
shutil.copy2(csv_path, backup_path) print(" Matching packets to queries...")
print(f" Backup: {backup_path.name}") t0 = time.time()
timestamps = [p.timestamp for p in packets]
matched = 0
for q in queries:
lo = bisect.bisect_left(timestamps, q["start_time"])
hi = bisect.bisect_right(timestamps, q["end_time"])
bs = br = ps = pr = 0
for pkt in packets[lo:hi]:
if pkt.is_outbound:
bs += pkt.size
ps += 1
else:
br += pkt.size
pr += 1
q["bytes_sent"] = bs
q["bytes_received"] = br
q["packets_sent"] = ps
q["packets_received"] = pr
q["total_bytes"] = bs + br
matched += ps + pr
print(f" Matched {matched:,} packets in {time.time() - t0:.2f}s")
total_sent = sum(q["bytes_sent"] for q in queries)
total_recv = sum(q["bytes_received"] for q in queries)
with_data = sum(1 for q in queries if q["total_bytes"] > 0)
print(f" Total: {total_sent:,} B sent, {total_recv:,} B received")
print(f" Queries with data: {with_data}/{len(queries)}")
return matched
def write_csv(csv_path: Path, queries: List[Dict], backup: bool = True):
if backup and csv_path.exists():
bak = csv_path.with_suffix(".csv.bak")
if not bak.exists():
shutil.copy2(csv_path, bak)
print(f" Backup: {bak.name}")
# Get fieldnames - filter out any existing bandwidth columns to avoid dupes
original_fields = [ original_fields = [
f for f in queries[0]['data'].keys() f for f in queries[0]["data"].keys() if f not in BANDWIDTH_COLUMNS
if f not in BANDWIDTH_COLUMNS
] ]
fieldnames = original_fields + BANDWIDTH_COLUMNS fieldnames = original_fields + BANDWIDTH_COLUMNS
with open(csv_path, 'w', encoding='utf-8', newline='') as f: with open(csv_path, "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames) writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader() writer.writeheader()
for q in queries: for q in queries:
row = {k: v for k, v in q['data'].items() if k not in BANDWIDTH_COLUMNS} row = {k: q["data"][k] for k in original_fields}
for field in BANDWIDTH_COLUMNS: for c in BANDWIDTH_COLUMNS:
row[field] = q[field] row[c] = q[c]
writer.writerow(row) writer.writerow(row)
print(f" Written: {csv_path.name}") print(f" Written: {csv_path.name}")
def process_provider_directory(provider_path: Path): def process_provider(provider_path: Path, local_ip_bytes: bytes):
"""Process all CSV/PCAP pairs in a provider directory.""" print(f"\n{'=' * 60}\nProcessing: {provider_path.name.upper()}\n{'=' * 60}")
print(f"\n{'='*60}")
print(f"Processing: {provider_path.name.upper()}")
print(f"{'='*60}")
csv_files = sorted(provider_path.glob('*.csv')) processed = skipped = 0
processed = 0 total_time = 0.0
skipped = 0
total_time = 0
for csv_path in csv_files: for csv_path in sorted(provider_path.glob("*.csv")):
# Skip backup files name = csv_path.name.lower()
if '.bak' in csv_path.name: if ".bak" in name or name.endswith((".cpu.csv", ".mem.csv")):
continue continue
pcap_path = csv_path.with_suffix('.pcap') pcap_path = csv_path.with_suffix(".pcap")
if not pcap_path.exists(): if not pcap_path.exists():
print(f"\n Skipping {csv_path.name} - no matching PCAP") print(f"\n{csv_path.name}: no matching PCAP")
continue continue
# Check if already processed if not needs_processing(csv_path):
if is_already_processed(csv_path): print(f"\n{csv_path.name}: already processed")
print(f"\n ⏭ Skipping {csv_path.name} - already processed")
skipped += 1 skipped += 1
continue continue
print(f"\n 📁 {csv_path.name}") print(f"\n 📁 {csv_path.name}")
file_start = time.time() t0 = time.time()
# Load PCAP into memory first packets = load_pcap(pcap_path, local_ip_bytes)
packets = load_pcap_into_memory(pcap_path)
if not packets: if not packets:
print(f" ⚠ No packets found in PCAP") print(" ⚠ No usable packets in PCAP")
continue continue
# Load CSV queries
queries = load_csv_queries(csv_path) queries = load_csv_queries(csv_path)
if not queries: if not queries:
print(f" ⚠ No valid queries found") print(" ⚠ No valid queries in CSV")
continue continue
print(f" Loaded {len(queries):,} queries") print(f" Loaded {len(queries):,} queries")
# Match packets to queries match_packets(packets, queries)
enriched_queries = match_packets_to_queries(packets, queries) write_csv(csv_path, queries)
# Write enriched CSV dt = time.time() - t0
write_enriched_csv(csv_path, enriched_queries) total_time += dt
file_time = time.time() - file_start
total_time += file_time
processed += 1 processed += 1
print(f" ✓ Completed in {file_time:.2f}s") print(f" ✓ Completed in {dt:.2f}s")
print(f"\n {'='*58}")
print(f" {provider_path.name}: {processed} processed, {skipped} skipped")
print(f" Time: {total_time:.2f}s")
print(f" {'='*58}")
print(
f"\n {provider_path.name}: {processed} processed, "
f"{skipped} skipped, {total_time:.2f}s"
)
def main(): def main():
"""Main preprocessing pipeline.""" ap = argparse.ArgumentParser(description=__doc__)
overall_start = time.time() ap.add_argument(
"--local-ip",
default=DEFAULT_LOCAL_IP,
help=f"Local (netns veth) IP used to determine direction "
f"(default: {DEFAULT_LOCAL_IP})",
)
ap.add_argument("--results-dir", default="results", type=Path)
ap.add_argument("--providers", nargs="+", default=DEFAULT_PROVIDERS)
args = ap.parse_args()
print("\n" + "="*60) local_ip_bytes = socket.inet_aton(args.local_ip)
print("DNS PCAP PREPROCESSOR - Memory-Optimized Edition")
print("="*60)
results_dir = Path('results') print(f"\n{'=' * 60}\nDNS PCAP PREPROCESSOR\n{'=' * 60}")
print(f"Local IP: {args.local_ip}")
print(f"Results: {args.results_dir}")
if not results_dir.exists(): if not args.results_dir.exists():
print(f"\nError: '{results_dir}' directory not found") print(f"\nDirectory not found: {args.results_dir}")
return return
providers = ['adguard', 'cloudflare', 'google', 'quad9'] t0 = time.time()
for provider in args.providers:
for provider in providers: path = args.results_dir / provider
provider_path = results_dir / provider if path.exists():
if provider_path.exists(): process_provider(path, local_ip_bytes)
process_provider_directory(provider_path)
else: else:
print(f"\nWarning: Provider directory not found: {provider}") print(f"\nMissing provider directory: {provider}")
overall_time = time.time() - overall_start total = time.time() - t0
print(f"\n{'=' * 60}")
print("\n" + "="*60) print(f"✓ DONE in {total:.2f}s ({total / 60:.1f} min)")
print(f"✓ PREPROCESSING COMPLETE") print(f"{'=' * 60}\n")
print(f" Total time: {overall_time:.2f}s ({overall_time/60:.1f} minutes)")
print("="*60 + "\n")
if __name__ == '__main__': if __name__ == "__main__":
main() main()
+5 -11
View File
@@ -74,6 +74,10 @@ def merge_all_csvs(input_dir: Path, output_path: Path):
csv_files = find_csv_files(input_dir) csv_files = find_csv_files(input_dir)
print(f"Files to process ({len(csv_files)}):")
for f in csv_files:
print(f" {f}")
if not csv_files: if not csv_files:
print("No CSV files found") print("No CSV files found")
return return
@@ -96,11 +100,6 @@ def merge_all_csvs(input_dir: Path, output_path: Path):
'duration_ms', 'duration_ms',
'request_size_bytes', 'request_size_bytes',
'response_size_bytes', 'response_size_bytes',
'bytes_sent',
'bytes_received',
'packets_sent',
'packets_received',
'total_bytes',
'response_code', 'response_code',
'error', 'error',
] ]
@@ -146,11 +145,6 @@ def merge_all_csvs(input_dir: Path, output_path: Path):
'duration_ms': ns_to_ms(row.get('duration_ns', '')), 'duration_ms': ns_to_ms(row.get('duration_ns', '')),
'request_size_bytes': row.get('request_size_bytes', ''), 'request_size_bytes': row.get('request_size_bytes', ''),
'response_size_bytes': row.get('response_size_bytes', ''), 'response_size_bytes': row.get('response_size_bytes', ''),
'bytes_sent': row.get('bytes_sent', ''),
'bytes_received': row.get('bytes_received', ''),
'packets_sent': row.get('packets_sent', ''),
'packets_received': row.get('packets_received', ''),
'total_bytes': row.get('total_bytes', ''),
'response_code': row.get('response_code', ''), 'response_code': row.get('response_code', ''),
'error': row.get('error', ''), 'error': row.get('error', ''),
} }
@@ -173,7 +167,7 @@ def main():
parser.add_argument( parser.add_argument(
'input_dir', 'input_dir',
nargs='?', nargs='?',
default='.', default='results',
help='Input directory containing provider folders (default: .)' help='Input directory containing provider folders (default: .)'
) )
parser.add_argument( parser.add_argument(
+1 -1
View File
@@ -50,7 +50,7 @@ def merge_mem_files(input_dir: Path, output_path: Path):
print(f"Found {len(mem_files)} Memory metric files") print(f"Found {len(mem_files)} Memory metric files")
output_columns = [ output_columns = [
'id',' provider', 'protocol', 'dnssec_mode', 'keep_alive', 'id','provider', 'protocol', 'dnssec_mode', 'keep_alive',
'timestamp', 'total_alloc_bytes', 'mallocs', 'gc_cycles', 'timestamp', 'total_alloc_bytes', 'mallocs', 'gc_cycles',
'alloc_delta', 'mallocs_delta', 'gc_delta' 'alloc_delta', 'mallocs_delta', 'gc_delta'
] ]
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
Summarize all .pcap files into a single unified PCAP metrics CSV.
Each row represents one scenario (one PCAP file): total bytes/packets sent and received.
Adds provider, protocol, dnssec_mode, keep_alive columns parsed from filenames.
"""
import argparse
import socket
import time
import csv
from pathlib import Path
from typing import List, NamedTuple
import dpkt
DEFAULT_LOCAL_IP = "192.168.100.2" # netns veth1 address
DEFAULT_PROVIDERS = ["adguard", "cloudflare", "google", "quad9"]
class PacketSummary(NamedTuple):
bytes_sent: int
bytes_received: int
packets_sent: int
packets_received: int
total_bytes: int
total_packets: int
def parse_config_from_filename(filename: str) -> dict:
"""Parse protocol, dnssec_mode, keep_alive from filename like 'dot-trust-persist.pcap'"""
base = filename.replace(".pcap", "").replace(".PCAP", "")
parts = base.split("-")
protocol = parts[0]
dnssec_mode = "off"
keep_alive = 0
for part in parts[1:]:
if part in ("auth", "trust"):
dnssec_mode = part
elif part == "persist":
keep_alive = 1
return {
"protocol": protocol,
"dnssec_mode": dnssec_mode,
"keep_alive": keep_alive,
}
def find_pcap_files(input_dir: Path, providers: List[str]) -> List[Path]:
files: List[Path] = []
for provider in providers:
provider_path = input_dir / provider
if not provider_path.exists():
print(f" ⚠ Missing provider directory: {provider}")
continue
for p in sorted(provider_path.glob("*.pcap")):
if ".bak" not in p.name:
files.append(p)
return files
def summarize_pcap(pcap_path: Path, local_ip_bytes: bytes) -> PacketSummary:
"""Read a PCAP file and return aggregated byte/packet counts by direction."""
bytes_sent = bytes_received = packets_sent = packets_received = 0
with open(pcap_path, "rb") as f:
try:
reader = dpkt.pcap.Reader(f)
except ValueError:
f.seek(0)
reader = dpkt.pcapng.Reader(f)
for _ts, buf in reader:
try:
eth = dpkt.ethernet.Ethernet(buf)
ip = eth.data
if not isinstance(ip, dpkt.ip.IP):
continue
size = len(buf)
if ip.src == local_ip_bytes:
bytes_sent += size
packets_sent += 1
elif ip.dst == local_ip_bytes:
bytes_received += size
packets_received += 1
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError, AttributeError):
continue
return PacketSummary(
bytes_sent=bytes_sent,
bytes_received=bytes_received,
packets_sent=packets_sent,
packets_received=packets_received,
total_bytes=bytes_sent + bytes_received,
total_packets=packets_sent + packets_received,
)
def merge_pcap_files(input_dir: Path, output_path: Path, local_ip: str, providers: List[str]):
local_ip_bytes = socket.inet_aton(local_ip)
pcap_files = find_pcap_files(input_dir, providers)
if not pcap_files:
print("No .pcap files found.")
return
print(f"Found {len(pcap_files)} PCAP files")
print(f"Local IP: {local_ip}\n")
output_columns = [
"id", "provider", "protocol", "dnssec_mode", "keep_alive",
"bytes_sent", "bytes_received", "packets_sent", "packets_received",
"total_bytes", "total_packets",
]
total_rows = 0
t_total = time.time()
with open(output_path, "w", newline="", encoding="utf-8") as outfile:
writer = csv.DictWriter(outfile, fieldnames=output_columns)
writer.writeheader()
for pcap_path in pcap_files:
provider = pcap_path.parent.name.lower()
config = parse_config_from_filename(pcap_path.name)
print(f" {provider}/{pcap_path.name} "
f"({config['protocol']}, {config['dnssec_mode']}, persist={config['keep_alive']})")
t0 = time.time()
summary = summarize_pcap(pcap_path, local_ip_bytes)
dt = time.time() - t0
total_rows += 1
writer.writerow({
"id": total_rows,
"provider": provider,
"protocol": config["protocol"],
"dnssec_mode": config["dnssec_mode"],
"keep_alive": config["keep_alive"],
"bytes_sent": summary.bytes_sent,
"bytes_received": summary.bytes_received,
"packets_sent": summary.packets_sent,
"packets_received": summary.packets_received,
"total_bytes": summary.total_bytes,
"total_packets": summary.total_packets,
})
print(f"{summary.packets_sent:,} sent / {summary.packets_received:,} received "
f"| {summary.total_bytes:,} B total ({dt:.2f}s)")
elapsed = time.time() - t_total
print(f"\n{'=' * 60}")
print(f"PCAP metrics merged → {output_path}")
print(f"Total scenarios: {total_rows}")
print(f"Completed in {elapsed:.2f}s ({elapsed / 60:.1f} min)")
print(f"{'=' * 60}")
def main():
parser = argparse.ArgumentParser(description="Summarize all .pcap files into a unified CSV.")
parser.add_argument("input_dir", nargs="?", default=".", help="Input directory (default: .)")
parser.add_argument("-o", "--output", default="dns_results_pcap.csv", help="Output CSV path")
parser.add_argument("--local-ip", default=DEFAULT_LOCAL_IP,
help=f"Local netns veth IP for direction detection (default: {DEFAULT_LOCAL_IP})")
parser.add_argument("--providers", nargs="+", default=DEFAULT_PROVIDERS,
help="Provider subdirectory names to scan")
args = parser.parse_args()
merge_pcap_files(Path(args.input_dir), Path(args.output), args.local_ip, args.providers)
return 0
if __name__ == "__main__":
exit(main())
+71
View File
@@ -0,0 +1,71 @@
import csv
import sys
from collections import defaultdict
COLUMNS = ["provider", "protocol", "dnssec_mode", "query_type", "keep_alive"]
def get_uncovered_values(rows, selected_indices, col_indices, all_values):
covered = defaultdict(set)
for idx in selected_indices:
for col, col_idx in col_indices.items():
covered[col].add(rows[idx][col_idx])
uncovered = {}
for col in COLUMNS:
uncovered[col] = all_values[col] - covered[col]
return uncovered
def main(input_file, output_file):
with open(input_file, newline="") as f:
reader = csv.reader(f)
header = next(reader)
rows = list(reader)
col_indices = {col: header.index(col) for col in COLUMNS}
# Collect all unique values per column
all_values = defaultdict(set)
for row in rows:
for col, idx in col_indices.items():
all_values[col].add(row[idx])
# Greedy set cover
selected = []
uncovered = get_uncovered_values(rows, selected, col_indices, all_values)
while any(uncovered.values()):
best_row = None
best_score = 0
for i, row in enumerate(rows):
if i in selected:
continue
score = sum(
1 for col, idx in col_indices.items() if row[idx] in uncovered[col]
)
if score > best_score:
best_score = score
best_row = i
if best_row is None:
break
selected.append(best_row)
uncovered = get_uncovered_values(rows, selected, col_indices, all_values)
with open(output_file, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(header)
for idx in selected:
writer.writerow(rows[idx])
print(f"Selected {len(selected)} rows out of {len(rows)}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python minimize_csv.py input.csv output.csv")
sys.exit(1)
main(sys.argv[1], sys.argv[2])
+126 -86
View File
@@ -10,6 +10,7 @@ OUTPUT_DIR="./results"
INTERFACE="veth1" INTERFACE="veth1"
TIMEOUT="5s" TIMEOUT="5s"
SLEEP_TIME="1" SLEEP_TIME="1"
DRY_RUN="false"
# Parse arguments # Parse arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -38,6 +39,10 @@ while [[ $# -gt 0 ]]; do
SLEEP_TIME="$2" SLEEP_TIME="$2"
shift 2 shift 2
;; ;;
-n|--dry-run)
DRY_RUN="true"
shift
;;
--help) --help)
echo "Usage: $0 [OPTIONS]" echo "Usage: $0 [OPTIONS]"
echo "" echo ""
@@ -48,6 +53,7 @@ while [[ $# -gt 0 ]]; do
echo " -I, --interface NAME Network interface (default: veth1)" echo " -I, --interface NAME Network interface (default: veth1)"
echo " -T, --timeout DURATION Timeout duration (default: 5s)" echo " -T, --timeout DURATION Timeout duration (default: 5s)"
echo " -s, --sleep SECONDS Sleep between runs (default: 1)" echo " -s, --sleep SECONDS Sleep between runs (default: 1)"
echo " -n, --dry-run Print the scenarios that would run, then exit"
echo " --help Show this help" echo " --help Show this help"
exit 0 exit 0
;; ;;
@@ -66,39 +72,102 @@ echo " Output dir: $OUTPUT_DIR"
echo " Interface: $INTERFACE" echo " Interface: $INTERFACE"
echo " Timeout: $TIMEOUT" echo " Timeout: $TIMEOUT"
echo " Sleep time: ${SLEEP_TIME}s" echo " Sleep time: ${SLEEP_TIME}s"
echo " Dry run: $DRY_RUN"
echo "" echo ""
# Server definitions as associative arrays (name -> url) SC_NAME=()
declare -A CONN_SERVERS=( SC_URL=()
["google-dotcp"]="dotcp://8.8.8.8:53" SC_DNSSEC=()
["cloudflare-dotcp"]="dotcp://1.1.1.1:53" SC_AUTH=()
["quad9-dotcp"]="dotcp://9.9.9.9:53" SC_KEEP=()
["adguard-dotcp"]="dotcp://dns.adguard-dns.com:53"
["google-dot"]="tls://8.8.8.8:853"
["cloudflare-dot"]="tls://1.1.1.1:853"
["quad9-dot"]="tls://9.9.9.9:853"
["adguard-dot"]="tls://dns.adguard-dns.com:853"
["google-doh"]="https://dns.google/dns-query"
["cloudflare-doh"]="https://cloudflare-dns.com/dns-query"
["quad9-doh"]="https://dns10.quad9.net/dns-query"
["adguard-doh"]="https://dns.adguard-dns.com/dns-query"
)
declare -A QUIC_SERVERS=( add() { # add <name> <url> <dnssec> <auth> <keepalive>
["google-doh3"]="doh3://dns.google/dns-query" SC_NAME+=("$1")
["cloudflare-doh3"]="doh3://cloudflare-dns.com/dns-query" SC_URL+=("$2")
["adguard-doh3"]="doh3://dns.adguard-dns.com/dns-query" SC_DNSSEC+=("$3")
["adguard-doq"]="doq://dns.adguard-dns.com:853" SC_AUTH+=("$4")
) SC_KEEP+=("$5")
}
declare -A CONNLESS_SERVERS=( # Protocol comparison - DNSSEC off, non-persistent.
["google-doudp"]="udp://8.8.8.8:53" # The primary comparison axis: full encryption cost, handshake included.
["cloudflare-doudp"]="udp://1.1.1.1:53" # All transports.
["quad9-doudp"]="udp://9.9.9.9:53" add google-dotcp "dotcp://8.8.8.8:53" false false false
["adguard-doudp"]="udp://dns.adguard-dns.com:53" add cloudflare-dotcp "dotcp://1.1.1.1:53" false false false
["adguard-dnscrypt"]="sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20" add quad9-dotcp "dotcp://9.9.9.9:53" false false false
["quad9-dnscrypt"]="sdns://AQMAAAAAAAAAFDE0OS4xMTIuMTEyLjExMjo4NDQzIGfIR7jIdYzRICRVQ751Z0bfNN8dhMALjEcDaN-CHYY-GTIuZG5zY3J5cHQtY2VydC5xdWFkOS5uZXQ" add adguard-dotcp "dotcp://dns.adguard-dns.com:53" false false false
)
add google-dot "tls://8.8.8.8:853" false false false
add cloudflare-dot "tls://1.1.1.1:853" false false false
add quad9-dot "tls://9.9.9.9:853" false false false
add adguard-dot "tls://dns.adguard-dns.com:853" false false false
add google-doh "https://dns.google/dns-query" false false false
add cloudflare-doh "https://cloudflare-dns.com/dns-query" false false false
add quad9-doh "https://dns10.quad9.net/dns-query" false false false
add adguard-doh "https://dns.adguard-dns.com/dns-query" false false false
add google-doh3 "doh3://dns.google/dns-query" false false false
add cloudflare-doh3 "doh3://cloudflare-dns.com/dns-query" false false false
add adguard-doh3 "doh3://dns.adguard-dns.com/dns-query" false false false
add adguard-doq "doq://dns.adguard-dns.com:853" false false false
add google-doudp "udp://8.8.8.8:53" false false false
add cloudflare-doudp "udp://1.1.1.1:53" false false false
add quad9-doudp "udp://9.9.9.9:53" false false false
add adguard-doudp "udp://dns.adguard-dns.com:53" false false false
add adguard-dnscrypt "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20" false false false
add quad9-dnscrypt "sdns://AQMAAAAAAAAAFDE0OS4xMTIuMTEyLjExMjo4NDQzIGfIR7jIdYzRICRVQ751Z0bfNN8dhMALjEcDaN-CHYY-GTIuZG5zY3J5cHQtY2VydC5xdWFkOS5uZXQ" false false false
# Persistence contrast - DNSSEC off, persistent.
# Teste for TCP based protocols.
# QUIC makes no sense cause of 0-RTT.
add google-dotcp "dotcp://8.8.8.8:53" false false true
add cloudflare-dotcp "dotcp://1.1.1.1:53" false false true
add quad9-dotcp "dotcp://9.9.9.9:53" false false true
add adguard-dotcp "dotcp://dns.adguard-dns.com:53" false false true
add google-dot "tls://8.8.8.8:853" false false true
add cloudflare-dot "tls://1.1.1.1:853" false false true
add quad9-dot "tls://9.9.9.9:853" false false true
add adguard-dot "tls://dns.adguard-dns.com:853" false false true
add google-doh "https://dns.google/dns-query" false false true
add cloudflare-doh "https://cloudflare-dns.com/dns-query" false false true
add quad9-doh "https://dns10.quad9.net/dns-query" false false true
add adguard-doh "https://dns.adguard-dns.com/dns-query" false false true
# DNSSEC trust - non-persistent.
add google-dotcp "dotcp://8.8.8.8:53" true false false
add cloudflare-dotcp "dotcp://1.1.1.1:53" true false false
add quad9-dotcp "dotcp://9.9.9.9:53" true false false
add adguard-dotcp "dotcp://dns.adguard-dns.com:53" true false false
add google-dot "tls://8.8.8.8:853" true false false
add cloudflare-dot "tls://1.1.1.1:853" true false false
add quad9-dot "tls://9.9.9.9:853" true false false
add adguard-dot "tls://dns.adguard-dns.com:853" true false false
add google-doh "https://dns.google/dns-query" true false false
add cloudflare-doh "https://cloudflare-dns.com/dns-query" true false false
add quad9-doh "https://dns10.quad9.net/dns-query" true false false
add adguard-doh "https://dns.adguard-dns.com/dns-query" true false false
add google-doh3 "doh3://dns.google/dns-query" true false false
add cloudflare-doh3 "doh3://cloudflare-dns.com/dns-query" true false false
add adguard-doh3 "doh3://dns.adguard-dns.com/dns-query" true false false
add adguard-doq "doq://dns.adguard-dns.com:853" true false false
add google-doudp "udp://8.8.8.8:53" true false false
add cloudflare-doudp "udp://1.1.1.1:53" true false false
add quad9-doudp "udp://9.9.9.9:53" true false false
add adguard-doudp "udp://dns.adguard-dns.com:53" true false false
add adguard-dnscrypt "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20" true false false
add quad9-dnscrypt "sdns://AQMAAAAAAAAAFDE0OS4xMTIuMTEyLjExMjo4NDQzIGfIR7jIdYzRICRVQ751Z0bfNN8dhMALjEcDaN-CHYY-GTIuZG5zY3J5cHQtY2VydC5xdWFkOS5uZXQ" true false false
# DNSSEC auth - DoUDP only.
# auth walks the chain of trust over plain DoUDP regardless of the protocol.
add adguard-doudp "udp://dns.adguard-dns.com:53" true true false
# Function to get flags suffix for filename # Function to get flags suffix for filename
get_flags_suffix() { get_flags_suffix() {
@@ -133,10 +202,6 @@ run_with_perf() {
local keepalive="$5" local keepalive="$5"
local suffix=$(get_flags_suffix "$dnssec" "$auth" "$keepalive") local suffix=$(get_flags_suffix "$dnssec" "$auth" "$keepalive")
local base_name="${name}"
if [[ -n "$suffix" ]]; then
base_name="${name}-${suffix}"
fi
local provider="${name%%-*}" local provider="${name%%-*}"
local protocol="${name#*-}" local protocol="${name#*-}"
local base_name="${protocol}" local base_name="${protocol}"
@@ -194,64 +259,39 @@ run_with_perf() {
# Cleanup # Cleanup
rm -f "$perf_tmp" rm -f "$perf_tmp"
echo " -> CPU metrics saved to ${base_name}.cpu.csv" echo " -> CPU metrics saved to ${provider}/${base_name}.cpu.csv"
} }
# Function to run servers with given flags echo "Total scenarios: ${#SC_NAME[@]}"
run_server_group() { echo ""
local -n servers=$1
local dnssec="$2"
local auth="$3"
local keepalive="$4"
local desc="$5"
echo "Running: $desc" for i in "${!SC_NAME[@]}"; do
name="${SC_NAME[$i]}"
url="${SC_URL[$i]}"
dnssec="${SC_DNSSEC[$i]}"
auth="${SC_AUTH[$i]}"
keepalive="${SC_KEEP[$i]}"
for name in "${!servers[@]}"; do suffix=$(get_flags_suffix "$dnssec" "$auth" "$keepalive")
local url="${servers[$name]}" provider="${name%%-*}"
echo " Processing: $name ($url)" protocol="${name#*-}"
label="${protocol}"
[[ -n "$suffix" ]] && label="${protocol}-${suffix}"
if [[ "$DRY_RUN" == "true" ]]; then
printf '[dry] %-28s dnssec=%-5s auth=%-5s keepalive=%-5s -> %s/%s\n' \
"$name" "$dnssec" "$auth" "$keepalive" "$provider" "$label"
continue
fi
echo "Processing: $name ($url) [dnssec=$dnssec auth=$auth keepalive=$keepalive]"
run_with_perf "$name" "$url" "$dnssec" "$auth" "$keepalive" run_with_perf "$name" "$url" "$dnssec" "$auth" "$keepalive"
sleep "$SLEEP_TIME" sleep "$SLEEP_TIME"
done done
}
echo "=== Running TCP-based protocols (TLS/HTTPS) ==="
# DNSSEC off, Keep off
run_server_group CONN_SERVERS "false" "false" "false" "no-dnssec, no-keepalive"
# DNSSEC off, Keep on
run_server_group CONN_SERVERS "false" "false" "true" "no-dnssec, keepalive"
# DNSSEC on (trust), Keep on
run_server_group CONN_SERVERS "true" "false" "true" "dnssec-trust, keepalive"
# DNSSEC on (auth), Keep on
run_server_group CONN_SERVERS "true" "true" "true" "dnssec-auth, keepalive"
echo "" echo ""
echo "=== Running QUIC-based protocols (DoH3/DoQ) ===" if [[ "$DRY_RUN" == "true" ]]; then
echo "Dry run complete - nothing was executed."
# DNSSEC off else
run_server_group QUIC_SERVERS "false" "false" "false" "no-dnssec" echo "All scenarios completed!"
fi
# DNSSEC on (trust)
run_server_group QUIC_SERVERS "true" "false" "false" "dnssec-trust"
# DNSSEC on (auth)
run_server_group QUIC_SERVERS "true" "true" "false" "dnssec-auth"
echo ""
echo "=== Running connectionless protocols (UDP) ==="
# DNSSEC off
run_server_group CONNLESS_SERVERS "false" "false" "false" "no-dnssec"
# DNSSEC on (trust)
run_server_group CONNLESS_SERVERS "true" "false" "false" "dnssec-trust"
# DNSSEC on (auth)
run_server_group CONNLESS_SERVERS "true" "true" "false" "dnssec-auth"
echo ""
echo "All combinations completed!"