feat(qol): add gol.go
This commit is contained in:
3
go.mod
3
go.mod
@@ -4,8 +4,10 @@ go 1.24.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/kong v1.8.1
|
github.com/alecthomas/kong v1.8.1
|
||||||
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/miekg/dns v1.1.63
|
github.com/miekg/dns v1.1.63
|
||||||
github.com/quic-go/quic-go v0.50.0
|
github.com/quic-go/quic-go v0.50.0
|
||||||
|
golang.org/x/net v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -17,7 +19,6 @@ require (
|
|||||||
golang.org/x/crypto v0.33.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||||
golang.org/x/mod v0.18.0 // indirect
|
golang.org/x/mod v0.18.0 // indirect
|
||||||
golang.org/x/net v0.35.0 // indirect
|
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
|||||||
14
go.sum
14
go.sum
@@ -18,6 +18,8 @@ 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/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
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/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
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/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=
|
||||||
@@ -41,25 +43,37 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
|
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.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
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-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
301
qol.go
Normal file
301
qol.go
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/afonsofrancof/sdns-proxy/client"
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/google/gopacket"
|
||||||
|
"github.com/google/gopacket/pcap"
|
||||||
|
"github.com/google/gopacket/pcapgo"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
Run RunCmd `cmd:"" help:"Run measurements for given servers and domains"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunCmd struct {
|
||||||
|
DomainsFile string `arg:"" help:"File with domains (one per line)"`
|
||||||
|
OutputDir string `short:"o" long:"output" default:"results" help:"Output directory"`
|
||||||
|
QueryType string `short:"t" long:"type" default:"A" help:"DNS query type"`
|
||||||
|
Repeat int `short:"r" long:"repeat" default:"5" help:"Queries per domain (sequential)"`
|
||||||
|
Timeout time.Duration `long:"timeout" default:"5s" help:"Query timeout (informational)"`
|
||||||
|
DNSSEC bool `long:"dnssec" help:"Enable DNSSEC"`
|
||||||
|
Interface string `long:"iface" default:"any" help:"Capture interface (e.g., eth0, any)"`
|
||||||
|
|
||||||
|
Servers []string `short:"s" long:"server" help:"Upstream servers (udp://..., tls://..., https://..., doq://...)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSMetric struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
QueryType string `json:"query_type"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
DNSSEC bool `json:"dnssec"`
|
||||||
|
DNSServer string `json:"dns_server"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Duration int64 `json:"duration_ns"`
|
||||||
|
DurationMs float64 `json:"duration_ms"`
|
||||||
|
RequestSize int `json:"request_size_bytes"`
|
||||||
|
ResponseSize int `json:"response_size_bytes"`
|
||||||
|
ResponseCode string `json:"response_code"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RunCmd) Run() error {
|
||||||
|
// Check if running with sufficient privileges for packet capture
|
||||||
|
if err := checkCapturePermissions(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
|
||||||
|
fmt.Fprintf(os.Stderr, "Packet capture may fail. Consider running as root/administrator.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
domains, err := readDomainsFile(r.DomainsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed reading domains: %w", err)
|
||||||
|
}
|
||||||
|
if len(r.Servers) == 0 {
|
||||||
|
return fmt.Errorf("at least one --server must be provided")
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(r.OutputDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("mkdir output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
qType, ok := dns.StringToType[strings.ToUpper(r.QueryType)]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid qtype: %s", r.QueryType)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, upstream := range r.Servers {
|
||||||
|
if err := r.runOne(upstream, domains, qType); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error on server %s: %v\n", upstream, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RunCmd) runOne(upstream string, domains []string, qType uint16) error {
|
||||||
|
opts := client.Options{DNSSEC: r.DNSSEC}
|
||||||
|
dnsClient, err := client.New(upstream, opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed creating client: %w", err)
|
||||||
|
}
|
||||||
|
defer dnsClient.Close()
|
||||||
|
|
||||||
|
// file naming
|
||||||
|
proto := detectProtocol(upstream)
|
||||||
|
ts := time.Now().Format("20060102_1504")
|
||||||
|
dnssecStr := "off"
|
||||||
|
if r.DNSSEC {
|
||||||
|
dnssecStr = "on"
|
||||||
|
}
|
||||||
|
base := fmt.Sprintf("%s_%s_dnssec_%s_%s",
|
||||||
|
proto, sanitize(upstream), dnssecStr, ts)
|
||||||
|
jsonPath := filepath.Join(r.OutputDir, base+".jsonl")
|
||||||
|
pcapPath := filepath.Join(r.OutputDir, base+".pcap")
|
||||||
|
|
||||||
|
fmt.Printf(">>> Measuring %s (dnssec=%v) → %s\n", upstream, r.DNSSEC, base)
|
||||||
|
|
||||||
|
// setup pcap capture
|
||||||
|
handle, err := pcap.OpenLive(r.Interface, 65535, true, pcap.BlockForever)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pcap open (try running as root): %w", err)
|
||||||
|
}
|
||||||
|
defer handle.Close()
|
||||||
|
|
||||||
|
pcapFile, err := os.Create(pcapPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create pcap file: %w", err)
|
||||||
|
}
|
||||||
|
defer pcapFile.Close()
|
||||||
|
|
||||||
|
writer := pcapgo.NewWriter(pcapFile)
|
||||||
|
if err := writer.WriteFileHeader(65535, handle.LinkType()); err != nil {
|
||||||
|
return fmt.Errorf("pcap header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
psrc := gopacket.NewPacketSource(handle, handle.LinkType())
|
||||||
|
pktCh := psrc.Packets()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var captureErr error
|
||||||
|
captureMutex := sync.Mutex{}
|
||||||
|
|
||||||
|
// Start packet capture goroutine
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case pkt, ok := <-pktCh:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ci := pkt.Metadata().CaptureInfo
|
||||||
|
if err := writer.WritePacket(ci, pkt.Data()); err != nil {
|
||||||
|
captureMutex.Lock()
|
||||||
|
if captureErr == nil {
|
||||||
|
captureErr = fmt.Errorf("pcap write error: %w", err)
|
||||||
|
}
|
||||||
|
captureMutex.Unlock()
|
||||||
|
fmt.Fprintf(os.Stderr, "pcap write error: %v\n", err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// open JSONL output
|
||||||
|
out, err := os.Create(jsonPath)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
wg.Wait()
|
||||||
|
return fmt.Errorf("create json out: %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
enc := json.NewEncoder(out)
|
||||||
|
|
||||||
|
// sequential measurement
|
||||||
|
for _, domain := range domains {
|
||||||
|
for rep := 0; rep < r.Repeat; rep++ {
|
||||||
|
metric := performQuery(dnsClient, domain, upstream, proto, qType, r.QueryType, r.DNSSEC)
|
||||||
|
if err := enc.Encode(metric); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "encode error: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ %s [%s] %s %.2fms\n",
|
||||||
|
metric.Domain, metric.Protocol, metric.ResponseCode, metric.DurationMs)
|
||||||
|
|
||||||
|
// Small delay to allow packet capture to catch up
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow some time for final packets to be captured
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Check if there were capture errors
|
||||||
|
captureMutex.Lock()
|
||||||
|
defer captureMutex.Unlock()
|
||||||
|
if captureErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: packet capture errors occurred: %v\n", captureErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func performQuery(dnsClient client.DNSClient, domain, upstream, proto string,
|
||||||
|
qType uint16, qTypeStr string, dnssec bool) DNSMetric {
|
||||||
|
|
||||||
|
metric := DNSMetric{
|
||||||
|
Domain: domain,
|
||||||
|
QueryType: qTypeStr,
|
||||||
|
Protocol: proto,
|
||||||
|
DNSSEC: dnssec,
|
||||||
|
DNSServer: upstream,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.Id = dns.Id()
|
||||||
|
msg.RecursionDesired = true
|
||||||
|
msg.SetQuestion(dns.Fqdn(domain), qType)
|
||||||
|
|
||||||
|
packed, err := msg.Pack()
|
||||||
|
if err != nil {
|
||||||
|
metric.ResponseCode = "ERROR"
|
||||||
|
metric.Error = fmt.Sprintf("pack request: %v", err)
|
||||||
|
return metric
|
||||||
|
}
|
||||||
|
metric.RequestSize = len(packed)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := dnsClient.Query(msg)
|
||||||
|
metric.Duration = time.Since(start).Nanoseconds()
|
||||||
|
metric.DurationMs = float64(metric.Duration) / 1e6
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
metric.ResponseCode = "ERROR"
|
||||||
|
metric.Error = err.Error()
|
||||||
|
return metric
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := resp.Pack()
|
||||||
|
if err != nil {
|
||||||
|
metric.ResponseCode = "ERROR"
|
||||||
|
metric.Error = fmt.Sprintf("pack response: %v", err)
|
||||||
|
return metric
|
||||||
|
}
|
||||||
|
|
||||||
|
metric.ResponseSize = len(respBytes)
|
||||||
|
metric.ResponseCode = dns.RcodeToString[resp.Rcode]
|
||||||
|
return metric
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDomainsFile(path string) ([]string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
var out []string
|
||||||
|
sc := bufio.NewScanner(f)
|
||||||
|
for sc.Scan() {
|
||||||
|
l := strings.TrimSpace(sc.Text())
|
||||||
|
if l != "" && !strings.HasPrefix(l, "#") {
|
||||||
|
out = append(out, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, sc.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitize(s string) string {
|
||||||
|
return strings.NewReplacer(":", "_", "/", "_").Replace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectProtocol(upstream string) string {
|
||||||
|
if strings.Contains(upstream, "://") {
|
||||||
|
u, err := url.Parse(upstream)
|
||||||
|
if err == nil && u.Scheme != "" {
|
||||||
|
return strings.ToLower(u.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "do53"
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCapturePermissions() error {
|
||||||
|
// Try to open a test interface to check permissions
|
||||||
|
handle, err := pcap.OpenLive("any", 65535, false, time.Millisecond*100)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "permission") ||
|
||||||
|
strings.Contains(err.Error(), "Operation not permitted") {
|
||||||
|
return fmt.Errorf("insufficient permissions for packet capture")
|
||||||
|
}
|
||||||
|
// Other errors might be due to interface availability, which is acceptable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
handle.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := kong.Parse(&CLI{},
|
||||||
|
kong.Name("dns-measurer"),
|
||||||
|
kong.Description("DNS secure protocols measurer with metrics + full pcap capture"),
|
||||||
|
kong.UsageOnError(),
|
||||||
|
)
|
||||||
|
err := ctx.Run()
|
||||||
|
ctx.FatalIfErrorf(err)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user