diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ccde91..ee22b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ adheres to [Semantic Versioning][semver]. ## [Unreleased] +### Added + +* Added `-f` / `--file` flag with which you can specify source file with + hostnames that will be queried. + [unreleased]: https://github.com/ameshkov/godnsbench/compare/v1.9.0...HEAD ## [1.9.0] - 2024-09-05 diff --git a/README.md b/README.md index f0aa88a..ed53b32 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,14 @@ Usage: godnsbench [OPTIONS] Application Options: - -a, --address= Address of the DNS server you're trying to test. Note, that - for encrypted DNS it should include the protocol (tls://, + -a, --address= Address of the DNS server you're trying to test. Note, that for encrypted DNS it should include the protocol (tls://, https://, quic://, h3://) - -p, --parallel= The number of connections you would like to open - simultaneously (default: 1) - -q, --query= The host name you would like to resolve. {random} will be - replaced with a random string (default: example.org) + -p, --parallel= The number of connections you would like to open simultaneously (default: 1) + -q, --query= The host name you would like to resolve. {random} will be replaced with a random string (default: example.org) + -f, --file= The path to the file with domain names to query -t, --timeout= Query timeout in seconds (default: 10) -r, --rate-limit= Rate limit (per second) (default: 0) - -c, --count= The overall number of queries we should send (default: - 10000) + -c, --count= The overall number of queries we should send (default: 10000) --insecure Do not validate the server certificate -v, --verbose Verbose output (optional) -o, --output= Path to the log file. If not set, write to stdout. diff --git a/go.mod b/go.mod index a7cdc00..9d4215d 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,13 @@ module github.com/ameshkov/godnsbench -go 1.22.6 - -toolchain go1.23.0 +go 1.23.3 require ( - github.com/AdguardTeam/dnsproxy v0.73.1 - github.com/AdguardTeam/golibs v0.27.0 + github.com/AdguardTeam/dnsproxy v0.73.4 + github.com/AdguardTeam/golibs v0.30.5 github.com/jessevdk/go-flags v1.6.1 github.com/miekg/dns v1.1.62 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 go.uber.org/ratelimit v0.3.1 ) @@ -30,14 +28,14 @@ require ( github.com/quic-go/qpack v0.5.0 // indirect github.com/quic-go/quic-go v0.46.0 // indirect go.uber.org/mock v0.4.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.27.0 // indirect gonum.org/v1/gonum v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 20be8c7..87a6864 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/AdguardTeam/dnsproxy v0.73.1 h1:4eMiaDm1NnowKltdx/LXLJ5FTTClBECPnLxtvJy3Q0Q= -github.com/AdguardTeam/dnsproxy v0.73.1/go.mod h1:ZcvmyQY2EiX5B0yCTkiYTgtm+1lBWA0lajbEI9dOhW4= -github.com/AdguardTeam/golibs v0.27.0 h1:YxCFK6HBGp/ZXp3bv5uei+oLH12UfIYB8u2rh1B6nnU= -github.com/AdguardTeam/golibs v0.27.0/go.mod h1:iWdjXPCwmK2g2FKIb/OwEPnovSXeMqRhI8FWLxF5oxE= +github.com/AdguardTeam/dnsproxy v0.73.4 h1:FTIXX34wQqePjtWUD1I4QfWTq2B2N1gfOW/TzZDdR4o= +github.com/AdguardTeam/dnsproxy v0.73.4/go.mod h1:18ssqhDgOCiVIwYmmVuXVM05wSwrzkO2yjKhVRWJX/g= +github.com/AdguardTeam/golibs v0.30.5 h1:xqat/N9o/V/AnakaWpqq+fGU/qJhKtL4A2pj66kC+TE= +github.com/AdguardTeam/golibs v0.30.5/go.mod h1:2wOvoAsubo/REnBiuu/YWYmkkzyFR52/QljMdQ2R58M= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= @@ -46,32 +46,32 @@ github.com/quic-go/qpack v0.5.0 h1:jldbr38Ef/swDfxtvNvvUIYNg5LNm3Oa9W+IZvCm4q0= github.com/quic-go/qpack v0.5.0/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y= github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= -golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 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/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0= gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/main.go b/main.go index dab75a3..dfb38bf 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,8 @@ import ( "syscall" "time" + "github.com/AdguardTeam/golibs/stringutil" + "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" goFlags "github.com/jessevdk/go-flags" @@ -42,6 +44,9 @@ type Options struct { // Query is the host name you would like to resolve during the bench. Query string `short:"q" long:"query" description:"The host name you would like to resolve. {random} will be replaced with a random string" default:"example.org"` + // QueriesPath is the path to the file with domain names to query. + QueriesPath string `short:"f" long:"file" description:"The path to the file with domain names to query"` + // Timeout is timeout for a query. Timeout int `short:"t" long:"timeout" description:"Query timeout in seconds" default:"10"` @@ -116,6 +121,11 @@ type runState struct { errors int // queriesToSend is the number of queries left to send. queriesToSend int + // queriesSent is the number of queries sent. + queriesSent int + + // hostnames is the list of hostnames to query. + hostnames []string // lastPrintedState is the last time we printed the intermediate state. // It is printed on every 100's query. @@ -133,6 +143,7 @@ func (r *runState) qpsTotal() (q float64) { defer r.m.Unlock() e := r.elapsed() + return float64(r.processed+r.errors) / e.Seconds() } @@ -148,15 +159,26 @@ func (r *runState) elapsedPerQuery() (e time.Duration) { if r.processed > 0 { avgElapsed = elapsed / time.Duration(r.processed) } + return avgElapsed } +// nextHostname returns the next hostname to be queried. +func (r *runState) nextHostname() (h string) { + r.m.Lock() + defer r.m.Unlock() + + return r.hostnames[r.queriesSent%len(r.hostnames)] +} + // incProcessed increments processed number, returns the new value. func (r *runState) incProcessed() (p int) { r.m.Lock() defer r.m.Unlock() + r.processed++ r.printIntermediateResults() + return r.processed } @@ -187,8 +209,10 @@ func (r *runState) printIntermediateResults() { func (r *runState) incErrors() (e int) { r.m.Lock() defer r.m.Unlock() + r.errors++ r.printIntermediateResults() + return r.errors } @@ -196,7 +220,10 @@ func (r *runState) incErrors() (e int) { func (r *runState) decQueriesToSend() (q int) { r.m.Lock() defer r.m.Unlock() + r.queriesToSend-- + r.queriesSent++ + return r.queriesToSend } @@ -234,10 +261,30 @@ func run(options *Options) (state *runState) { rate = ratelimit.NewUnlimited() } + var hostnames []string + + if options.QueriesPath != "" { + log.Info("Reading hostnames from the file %s", options.QueriesPath) + + b, err := os.ReadFile(options.QueriesPath) + if err != nil { + log.Fatalf("Failed to read from %s: %v", options.QueriesPath, err) + } + + hostnames = stringutil.SplitTrimmed(string(b), "\n") + } else { + hostnames = []string{options.Query} + } + + if len(hostnames) == 0 { + log.Fatalf("Empty list of hostnames in the file %s", options.QueriesPath) + } + state = &runState{ startTime: time.Now(), queriesToSend: options.QueriesCount + 1, rate: rate, + hostnames: hostnames, } // Subscribe to the bench run close event. @@ -283,13 +330,11 @@ func runConnection(options *Options, state *runState) { }, ) - randomize := strings.Contains(options.Query, "{random}") - queriesToSend := state.decQueriesToSend() for queriesToSend > 0 { - domainName := options.Query + domainName := state.nextHostname() - if randomize { + if strings.Contains(domainName, "{random}") { domainName = strings.ReplaceAll(domainName, "{random}", randString(randomLen)) } diff --git a/main_test.go b/main_test.go index 0fcc70d..5354f7d 100644 --- a/main_test.go +++ b/main_test.go @@ -12,6 +12,8 @@ import ( "fmt" "math/big" "net" + "os" + "path" "testing" "time" @@ -59,6 +61,54 @@ func Test_run(t *testing.T) { require.Equal(t, 0, state.errors) } +func Test_runWithQueriesFile(t *testing.T) { + str := `example.org +example.com +example.net` + filePath := path.Join(os.TempDir(), "queries.txt") + err := os.WriteFile(filePath, []byte(str), 0644) + require.NoError(t, err) + + defer func() { + _ = os.Remove(filePath) + }() + + tlsConfig, _ := createServerTLSConfig(t, "example.org") + p := createTestProxy(t, tlsConfig) + + p.RequestHandler = func(_ *proxy.Proxy, d *proxy.DNSContext) (err error) { + resp := &dns.Msg{} + resp.SetReply(d.Req) + d.Res = resp + + return nil + } + + err = p.Start(context.Background()) + require.NoError(t, err) + testutil.CleanupAndRequireSuccess(t, func() (err error) { + return p.Shutdown(context.Background()) + }) + + addr := p.Addr(proxy.ProtoHTTPS) + serverAddress := fmt.Sprintf("https://%s/dns-query", addr) + + o := &Options{ + Address: serverAddress, + Connections: 1, + QueriesPath: filePath, + Timeout: 10, + Rate: 50, + QueriesCount: 100, + InsecureSkipVerify: true, + } + + state := run(o) + + require.Equal(t, o.QueriesCount, state.processed) + require.Equal(t, 0, state.errors) +} + // createTestProxy creates a test DNS proxy that listens to all protocols. func createTestProxy(t *testing.T, tlsConfig *tls.Config) (p *proxy.Proxy) { listenIP := "127.0.0.1"