diff --git a/README.md b/README.md index cf3219b..8c9b4cc 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,158 @@ include `asn`. Unmapped countries use `ZZ`; unmapped ASNs use `0`. - `dnstt_bytes_out_total` - `dnstt_sessions_total` +## Grafana Queries + +Use `$domain` as a Grafana variable for the DNSTT domain, for example +`t2.bypasscensorship.org`. Replace `$__rate_interval` with a fixed range such +as `5m` if you are not using Grafana's built-in interval variables. + +- Title: Current active clients +- Description: Current number of active DNSTT sessions for the selected domain. +- Visualization: Stat or gauge. + +```promql +sum(dnstt_active_clients{domain="$domain"}) +``` + +- Title: Peak active clients +- Description: Highest active DNSTT session count observed since the exporter +started. +- Visualization: Stat. + +```promql +sum(dnstt_peak_clients{domain="$domain"}) +``` + +- Title: Active clients by country +- Description: Current active DNSTT sessions grouped by resolver country. +- Visualization: Geomap, bar chart, or table. + +```promql +sum by (country) (dnstt_active_clients{domain="$domain"}) +``` + +- Title: Top countries by active clients +- Description: Countries with the most active DNSTT sessions right now. +- Visualization: Bar chart. + +```promql +topk(10, sum by (country) (dnstt_active_clients{domain="$domain"})) +``` + +- Title: Active clients by ASN +- Description: Current active DNSTT sessions grouped by resolver ASN. +- Visualization: Bar chart or table. + +```promql +sum by (asn) (dnstt_active_clients{domain="$domain"}) +``` + +- Title: Top ASNs by active clients +- Description: Resolver ASNs with the most active DNSTT sessions right now. +- Visualization: Bar chart. + +```promql +topk(10, sum by (asn) (dnstt_active_clients{domain="$domain"})) +``` + +- Title: Active clients by country and ASN +- Description: Current active DNSTT sessions split by both resolver country and +resolver ASN. +- Visualization: Table. + +```promql +sum by (country, asn) (dnstt_active_clients{domain="$domain"}) +``` + +- Title: Top country/ASN pairs by active clients +- Description: Country and ASN combinations with the most active DNSTT sessions +right now. +- Visualization: Bar chart or table. + +```promql +topk(20, sum by (country, asn) (dnstt_active_clients{domain="$domain"})) +``` + +- Title: DNS query rate +- Description: Total observed DNSTT DNS queries per second for the selected +domain. +- Visualization: Time series. + +```promql +sum(rate(dnstt_queries_total{domain="$domain"}[$__rate_interval])) +``` + +- Title: DNS query rate by country +- Description: Observed DNSTT DNS queries per second grouped by resolver country. +- Visualization: Stacked time series or bar chart. Use stacked time series for +trends over time, and bar chart for current top countries. + +```promql +sum by (country) (rate(dnstt_queries_total{domain="$domain"}[$__rate_interval])) +``` + +- Title: Top ASNs by DNS query rate +- Description: Resolver ASNs producing the highest DNSTT DNS query rates. +- Visualization: Bar chart. + +```promql +topk(10, sum by (asn) (rate(dnstt_queries_total{domain="$domain"}[$__rate_interval]))) +``` + +- Title: Inbound DNS traffic rate +- Description: Bytes per second received in DNSTT DNS queries. +- Visualization: Time series. + +```promql +sum(rate(dnstt_bytes_in_total{domain="$domain"}[$__rate_interval])) +``` + +- Title: Outbound DNS traffic rate +- Description: Bytes per second sent in DNSTT DNS responses. +- Visualization: Time series. + +```promql +sum(rate(dnstt_bytes_out_total{domain="$domain"}[$__rate_interval])) +``` + +- Title: DNS traffic rate by country +- Description: Combined inbound and outbound DNSTT DNS bytes per second grouped +by resolver country. +- Visualization: Geomap, stacked time series, or bar chart. + +```promql +sum by (country) ( + rate(dnstt_bytes_in_total{domain="$domain"}[$__rate_interval]) + + + rate(dnstt_bytes_out_total{domain="$domain"}[$__rate_interval]) +) +``` + +- Title: Total observed sessions +- Description: Total unique DNSTT sessions observed since the exporter started. +- Visualization: Stat. + +```promql +sum(dnstt_sessions_total{domain="$domain"}) +``` + +- Title: New session rate +- Description: New DNSTT sessions observed per second. +- Visualization: Time series. + +```promql +sum(rate(dnstt_sessions_total{domain="$domain"}[$__rate_interval])) +``` + +- Title: New session rate by country +- Description: New DNSTT sessions per second grouped by resolver country. +- Visualization: Stacked time series, geomap, or bar chart. + +```promql +sum by (country) (rate(dnstt_sessions_total{domain="$domain"}[$__rate_interval])) +``` + ## Development ```sh diff --git a/flake.lock b/flake.lock index 24eab79..be257e5 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,18 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1777578337, - "narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=", - "rev": "15f4ee454b1dce334612fa6843b3e05cf546efab", - "revCount": 990025, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.990025%2Brev-15f4ee454b1dce334612fa6843b3e05cf546efab/019de756-85a1-7400-84a3-d277a7ed191b/source.tar.gz" + "lastModified": 1779622335, + "narHash": "sha256-ViA62qtL5za7V3d5I8OA9q9JcFhsVAiL5jVHwEclWqk=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "705e9929918b43bd7b715dc0a878ac870449bb03", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1" + "owner": "nixos", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" } }, "root": { diff --git a/flake.nix b/flake.nix index af586d9..ca0eb0b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1"; + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05"; outputs = { self, nixpkgs }: let diff --git a/internal/dnstt/collector.go b/internal/dnstt/collector.go index 4928c16..175b712 100644 --- a/internal/dnstt/collector.go +++ b/internal/dnstt/collector.go @@ -167,9 +167,12 @@ func (c *Collector) RecordQueryFrom(domain string, clientID string, resolverIP n now := c.now() client, exists := tunnel.clients[clientID] + updatePeak := !exists if !exists { client = &clientState{firstSeen: now, firstKey: key} tunnel.clients[clientID] = client + } else if now.Sub(client.lastSeen) >= ClientTimeout || client.lastKey != key { + updatePeak = true } client.lastSeen = now client.lastKey = key @@ -178,7 +181,9 @@ func (c *Collector) RecordQueryFrom(domain string, clientID string, resolverIP n client.bytesIn += uint64(size) } - updatePeaks(tunnel, now) + if updatePeak { + updatePeaks(tunnel, now) + } } // RecordResponse records an observed DNSTT DNS response. @@ -211,7 +216,6 @@ func (c *Collector) Snapshot() Snapshot { snapshot := Snapshot{Tunnels: make(map[string]TunnelSnapshot, len(c.tunnels))} for _, domain := range c.domains { tunnel := c.tunnels[domain] - updatePeaks(tunnel, now) series := c.seriesSnapshotsLocked(domain, tunnel, now) tunnelSnapshot := TunnelSnapshot{Domain: domain, Series: series} diff --git a/internal/dnstt/collector_test.go b/internal/dnstt/collector_test.go index d46c923..8f31044 100644 --- a/internal/dnstt/collector_test.go +++ b/internal/dnstt/collector_test.go @@ -1,6 +1,7 @@ package dnstt import ( + "net/netip" "testing" "time" ) @@ -59,3 +60,36 @@ func TestCollectorMatchesSubdomainsToRegisteredTunnel(t *testing.T) { t.Fatalf("active clients = %d, want 1", tunnel.ActiveClients) } } + +func TestCollectorUpdatesPeakWhenActiveClientChangesGeoKey(t *testing.T) { + now := time.Unix(1000, 0) + firstResolver := netip.MustParseAddr("192.0.2.53") + secondResolver := netip.MustParseAddr("198.51.100.53") + c := NewCollector( + []string{"tunnel.example.com"}, + WithNow(func() time.Time { return now }), + WithGeoResolver(fakeGeoResolver{ + labelNames: []string{"asn"}, + labels: map[netip.Addr]GeoLabels{ + firstResolver: {ASN: "64500"}, + secondResolver: {ASN: "64501"}, + }, + }), + ) + + c.RecordQueryFrom("tunnel.example.com", "client-a", firstResolver, 120) + c.RecordQueryFrom("tunnel.example.com", "client-a", secondResolver, 120) + + foundChangedASN := false + for _, series := range c.Snapshot().Tunnels["tunnel.example.com"].Series { + if series.ASN == "64501" && series.PeakClients != 1 { + t.Fatalf("peak clients for changed ASN = %d, want 1", series.PeakClients) + } + if series.ASN == "64501" { + foundChangedASN = true + } + } + if !foundChangedASN { + t.Fatal("series for changed ASN not found") + } +}