From bf4f039001e2b16b53824dcfe497525638b19c4e Mon Sep 17 00:00:00 2001 From: Brian Cunnie Date: Wed, 19 Jan 2022 06:47:21 -0800 Subject: [PATCH] Metrics are served via `metrics.status.sslip.io` - The metrics aren't fleshed out. In fact, there's only two so far: 1. uptime 2. number of queries - Even though the metrics aren't complete, I'm checking it in because this commit is already much too big. - I moved the version information to `version.status.sslip.io`; previously it was at `version.sslip.io`. I didn't want one endpoint for both metrics & version (worry: DNS amplification), and I wanted a consistent subdomain to find that information (i.e. `status.sslip.io`). - I'm not worried about atomic updates to the metrics; if a metric is off by one, if I skip a count because two lookups are happening at the exact same time, I don't care. - The `Metrics` struct is a pointer within `Xip` because I might have several copies of `Xip` (if I'm binding to several interfaces individually), but I must only have one copy of `Metrics` - I only include the metrics I'm interested in, usually because it took some work to implement that feature. I don't care about MX records, but I care about IPv6 lookups, DNS-01 challenges, public IP lookups. - got rid of a section of unreachable code at the end of `ProcessQuestion()`; I was tired of Goland flagging it. I had it there mostly because I was paranoid of falling through a `switch` statement --- bosh-release/src/sslip.io-dns-server/go.mod | 2 +- bosh-release/src/sslip.io-dns-server/go.sum | 4 +- .../integration_metrics_test.go | 58 ++++++++++++++++ .../sslip.io-dns-server/integration_test.go | 6 +- bosh-release/src/sslip.io-dns-server/main.go | 11 +-- .../src/sslip.io-dns-server/xip/xip.go | 67 ++++++++++++++++--- docs/DEVELOPER.md | 8 +-- k8s/document_root/index.html | 27 ++++++-- 8 files changed, 156 insertions(+), 27 deletions(-) create mode 100644 bosh-release/src/sslip.io-dns-server/integration_metrics_test.go diff --git a/bosh-release/src/sslip.io-dns-server/go.mod b/bosh-release/src/sslip.io-dns-server/go.mod index 2880ebd..1ee436e 100644 --- a/bosh-release/src/sslip.io-dns-server/go.mod +++ b/bosh-release/src/sslip.io-dns-server/go.mod @@ -25,7 +25,7 @@ require ( golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.8 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0 // indirect + google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 // indirect google.golang.org/grpc v1.43.0 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/bosh-release/src/sslip.io-dns-server/go.sum b/bosh-release/src/sslip.io-dns-server/go.sum index 8a9691f..d0e272e 100644 --- a/bosh-release/src/sslip.io-dns-server/go.sum +++ b/bosh-release/src/sslip.io-dns-server/go.sum @@ -301,8 +301,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0 h1:aCsSLXylHWFno0r4S3joLpiaWayvqd2Mn4iSvx4WZZc= -google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/bosh-release/src/sslip.io-dns-server/integration_metrics_test.go b/bosh-release/src/sslip.io-dns-server/integration_metrics_test.go new file mode 100644 index 0000000..1dadf99 --- /dev/null +++ b/bosh-release/src/sslip.io-dns-server/integration_metrics_test.go @@ -0,0 +1,58 @@ +package main_test + +import ( + "fmt" + "os/exec" + "strings" + "time" + "xip/xip" + + . "github.com/onsi/ginkgo/v2" + + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("IntegrationMetrics", func() { + var digCmd *exec.Cmd + var digSession *Session + var digArgs string + + When("the server is queried", func() { + It("should update metrics", func() { + startMetrics := getMetrics() + digArgs = "@localhost non-existent.sslip.io +short" + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + // we want to make sure digSession has exited because we + // want to parse the _full_ contents of stdout + Eventually(digSession, 1).Should(Exit(0)) + expectedMetrics := startMetrics + expectedMetrics.Queries += 2 // two queries: nonexistent.sslip.io, metrics.status.sslip.io + actualMetrics := getMetrics() + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + }) + }) +}) + +func getMetrics() (m xip.Metrics) { + digArgs := "@localhost metrics.status.sslip.io txt +short" + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + stdout, err := digCmd.Output() + Expect(err).ToNot(HaveOccurred()) + var uptime int + var junk string + _, err = fmt.Sscanf(string(stdout), + "\"uptime (seconds): %d\"\n"+ + "\"key-value store: %s\n"+ // %s "swallows" the double-quote at the end + "\"queries: %d\"\n", + &uptime, + &junk, + &m.Queries, + ) + Expect(err).ToNot(HaveOccurred()) + m.Start = time.Now().Add(-time.Duration(uptime) * time.Second) + //_, err = fmt.Fscanf(digSession.Out, "queries: %d", &m.Queries) + return m +} diff --git a/bosh-release/src/sslip.io-dns-server/integration_test.go b/bosh-release/src/sslip.io-dns-server/integration_test.go index 1d07422..ac6bff8 100644 --- a/bosh-release/src/sslip.io-dns-server/integration_test.go +++ b/bosh-release/src/sslip.io-dns-server/integration_test.go @@ -109,10 +109,10 @@ var _ = Describe("sslip.io-dns-server", func() { "@localhost example.com srv +short", `\A\z`, `TypeSRV example.com. \? nil, SOA example.com. briancunnie.gmail.com. 2021123100 900 900 1800 300\n$`), - Entry(`TXT for version.sslip.io is the version number of the xip software (which gets overwritten during linking)`, - "@127.0.0.1 version.sslip.io txt +short", + Entry(`TXT for version.status.sslip.io is the version number of the xip software (which gets overwritten during linking)`, + "@127.0.0.1 version.status.sslip.io txt +short", `\A"dev"\n"today"\n"xxx"\n\z`, - `TypeTXT version.sslip.io. \? \["dev"\], \["today"\], \["xxx"\]`), + `TypeTXT version.status.sslip.io. \? \["dev"\], \["today"\], \["xxx"\]`), Entry(`TXT is the querier's IPv4 address and the domain "ip.sslip.io"`, "@127.0.0.1 ip.sslip.io txt +short", `127.0.0.1`, diff --git a/bosh-release/src/sslip.io-dns-server/main.go b/bosh-release/src/sslip.io-dns-server/main.go index aaa88ef..ca6271d 100644 --- a/bosh-release/src/sslip.io-dns-server/main.go +++ b/bosh-release/src/sslip.io-dns-server/main.go @@ -33,12 +33,14 @@ func main() { } // I don't need to `defer etcdCli.Close()` it's redundant in the main routine: when main() exits, everything is closed. conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 53}) + // set up our global metrics struct, setting our start time + xipMetrics := xip.Metrics{Start: time.Now()} // common err hierarchy: net.OpError → os.SyscallError → syscall.Errno switch { case err == nil: log.Println(`Successfully bound to all interfaces, port 53.`) wg.Add(1) - readFrom(conn, etcdCli, &wg) + readFrom(conn, &wg, etcdCli, &xipMetrics) case isErrorPermissionsError(err): log.Println("Try invoking me with `sudo` because I don't have permission to bind to port 53.") log.Fatal(err.Error()) @@ -62,7 +64,7 @@ func main() { } else { wg.Add(1) boundIPsPorts = append(boundIPsPorts, conn.LocalAddr().String()) - go readFrom(conn, etcdCli, &wg) + go readFrom(conn, &wg, etcdCli, &xipMetrics) } } if len(boundIPsPorts) > 0 { @@ -77,7 +79,7 @@ func main() { wg.Wait() } -func readFrom(conn *net.UDPConn, etcdCli *clientv3.Client, wg *sync.WaitGroup) { +func readFrom(conn *net.UDPConn, wg *sync.WaitGroup, etcdCli xip.V3client, xipMetrics *xip.Metrics) { defer wg.Done() for { query := make([]byte, 512) @@ -87,7 +89,8 @@ func readFrom(conn *net.UDPConn, etcdCli *clientv3.Client, wg *sync.WaitGroup) { continue } go func() { - response, logMessage, err := xip.Xip{SrcAddr: addr.IP, Etcd: etcdCli}.QueryResponse(query) + xipServer := xip.Xip{SrcAddr: addr.IP, Etcd: etcdCli, Metrics: xipMetrics} + response, logMessage, err := xipServer.QueryResponse(query) if err != nil { log.Println(err.Error()) return diff --git a/bosh-release/src/sslip.io-dns-server/xip/xip.go b/bosh-release/src/sslip.io-dns-server/xip/xip.go index c546819..f0398b5 100644 --- a/bosh-release/src/sslip.io-dns-server/xip/xip.go +++ b/bosh-release/src/sslip.io-dns-server/xip/xip.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net" + "reflect" "regexp" "strconv" "strings" @@ -33,6 +34,18 @@ type V3client interface { type Xip struct { SrcAddr net.IP Etcd V3client + Metrics *Metrics +} + +type Metrics struct { + Start time.Time + Queries int + SuccessfulQueries int + SuccessfulAQueries int + SuccessfulAAAAQueries int + SuccessfulTXTSrcIPQueries int + SuccessfulTXTVersionQueries int + SuccessfulTXTDNS01ChallengeQUeries int } // DomainCustomization is a value that is returned for a specific query. @@ -48,7 +61,7 @@ type DomainCustomization struct { AAAA []dnsmessage.AAAAResource CNAME dnsmessage.CNAMEResource MX []dnsmessage.MXResource - TXT func(string) ([]dnsmessage.TXTResource, error) + TXT func(Xip) ([]dnsmessage.TXTResource, error) // Unlike the other record types, TXT is a function in order to enable more complex behavior // e.g. IP address of the query's source } @@ -115,7 +128,7 @@ var ( MX: mx2, }, }, - TXT: func(_ string) ([]dnsmessage.TXTResource, error) { + TXT: func(_ Xip) ([]dnsmessage.TXTResource, error) { // Although multiple TXT records with multiple strings are allowed, we're sticking // with a multiple TXT records with a single string apiece because that's what ProtonMail requires // and that's what google.com does. @@ -161,8 +174,8 @@ var ( "ip.sslip.io.": { TXT: ipSslipIo, }, - "version.sslip.io.": { - TXT: func(_ string) ([]dnsmessage.TXTResource, error) { + "version.status.sslip.io.": { + TXT: func(_ Xip) ([]dnsmessage.TXTResource, error) { return []dnsmessage.TXTResource{ {TXT: []string{VersionSemantic}}, // e.g. "2.2.1' {TXT: []string{VersionDate}}, // e.g. "2021/10/03-15:08:54+0100" @@ -170,6 +183,9 @@ var ( }, nil }, }, + "metrics.status.sslip.io.": { + TXT: metricsSslipIo, + }, } ) @@ -220,6 +236,7 @@ func (x Xip) QueryResponse(queryBytes []byte) (responseBytes []byte, logMessage response.Header.ID = queryHeader.ID response.Header.RecursionDesired = queryHeader.RecursionDesired + x.Metrics.Queries += 1 b := dnsmessage.NewBuilder(nil, response.Header) b.EnableCompression() if err = b.StartQuestions(); err != nil { @@ -538,8 +555,6 @@ func (x Xip) processQuestion(q dnsmessage.Question) (response Response, logMessa return response, logMessage + "nil, SOA " + soaLogMessage(soaResource), nil } } - // The following is flagged as "Unreachable code" in Goland, and that's expected - return response, "", errors.New("unexpectedly fell through x.processQuestion()") } // NSResponse sets the Answers/Authorities depending whether we're delegating or authoritative @@ -720,7 +735,7 @@ func (x Xip) TXTResources(fqdn string) ([]dnsmessage.TXTResource, error) { if domain, ok := Customizations[strings.ToLower(fqdn)]; ok { // Customizations[strings.ToLower(fqdn)] returns a _function_, // we call that function, which has the same return signature as this method - return domain.TXT(x.SrcAddr.String()) + return domain.TXT(x) } return nil, nil } @@ -750,8 +765,28 @@ func SOAResource(name dnsmessage.Name) dnsmessage.SOAResource { } // when TXT for "ip.sslip.io" is queried, return the IP address of the querier -func ipSslipIo(sourceIP string) ([]dnsmessage.TXTResource, error) { - return []dnsmessage.TXTResource{{TXT: []string{sourceIP}}}, nil +func ipSslipIo(x Xip) ([]dnsmessage.TXTResource, error) { + return []dnsmessage.TXTResource{{TXT: []string{x.SrcAddr.String()}}}, nil +} + +// when TXT for "metrics.sslip.io" is queried, return the cumulative metrics +func metricsSslipIo(x Xip) (txtResources []dnsmessage.TXTResource, err error) { + var metrics []string + uptime := time.Since(x.Metrics.Start) + metrics = append(metrics, fmt.Sprintf("uptime (seconds): %.0f", uptime.Seconds())) + keyValueStore := "etcd" + // comparing interfaces to nil are tricky: interfaces contain both a type + // and a value, and although the value is nil the type isn't, so we need the following + if x.Etcd == nil || reflect.ValueOf(x.Etcd).IsNil() { + keyValueStore = "builtin" + } + metrics = append(metrics, "key-value store: "+keyValueStore) + metrics = append(metrics, fmt.Sprintf("queries: %d", x.Metrics.Queries)) + metrics = append(metrics, fmt.Sprintf("queries/second: %.1f", float64(x.Metrics.Queries)/uptime.Seconds())) + for _, metric := range metrics { + txtResources = append(txtResources, dnsmessage.TXTResource{TXT: []string{metric}}) + } + return txtResources, nil } // when TXT for "k-v.io" is queried, return the key-value pair @@ -868,3 +903,17 @@ func soaLogMessage(soaResource dnsmessage.SOAResource) string { strconv.Itoa(int(soaResource.Expire)) + " " + strconv.Itoa(int(soaResource.MinTTL)) } + +// MostlyEquals compares all fields except `Start` (timestamp) +func (a Metrics) MostlyEquals(b Metrics) bool { + if a.Queries == b.Queries && + a.SuccessfulQueries == b.SuccessfulQueries && + a.SuccessfulAQueries == b.SuccessfulAQueries && + a.SuccessfulAAAAQueries == b.SuccessfulAAAAQueries && + a.SuccessfulTXTSrcIPQueries == b.SuccessfulTXTSrcIPQueries && + a.SuccessfulTXTVersionQueries == b.SuccessfulTXTVersionQueries && + a.SuccessfulTXTDNS01ChallengeQUeries == b.SuccessfulTXTDNS01ChallengeQUeries { + return true + } + return false +} diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index ca2bff2..3b67795 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -8,7 +8,7 @@ export OLD_VERSION=2.2.4 export VERSION=2.3.0 cd ~/workspace/sslip.io git pull -r --autostash -# update the version number for the TXT record for version.sslip.io +# update the version number for the TXT record for version.status.sslip.io sed -i '' "s/$OLD_VERSION/$VERSION/g" \ bin/make_all \ bosh-release/packages/sslip.io-dns-server/packaging \ @@ -17,7 +17,7 @@ sed -i '' "s/$OLD_VERSION/$VERSION/g" \ sed -i '' "s~/$OLD_VERSION/~/$VERSION/~g" \ k8s/document_root/index.html \ k8s/Dockerfile-sslip.io-dns-server -# update the git hash for the TXT record for version.sslip.io for BOSH release +# update the git hash for the TXT record for version.status.sslip.io for BOSH release sed -i '' "s/VersionGitHash=[0-9a-fA-F]*/VersionGitHash=$(git rev-parse --short HEAD)/g" \ bosh-release/packages/sslip.io-dns-server/packaging cd bosh-release/ @@ -49,7 +49,7 @@ dig +short sSlIp.Io echo 78.46.204.247 dig @$IP txt ip.sslip.io +short | tr -d '"' curl curlmyip.org; echo -dig @$IP txt version.sslip.io +short | grep $VERSION +dig @$IP txt version.status.sslip.io +short | grep $VERSION echo "\"$VERSION\"" dig @$IP my-key.kv.sslip.io txt +short # returns nothing echo " ===" # separator because the results are too similar @@ -101,7 +101,7 @@ git pull -r nvim sslip.io.yml bosh -e vsphere -d sslip.io deploy sslip.io.yml -l <(lpass show --note deployments.yml) --no-redact dig @ns-azure 127-0-0-1.sslip.io +short # output should be 127.0.0.1 -dig @ns-azure.nono.io txt version.sslip.io +short +dig @ns-azure.nono.io txt version.status.sslip.io +short git add -p git ci -v -m"Bump sslip.io BOSH release: $OLD_VERSION → $VERSION" git push diff --git a/k8s/document_root/index.html b/k8s/document_root/index.html index 3be9336..13f24cc 100644 --- a/k8s/document_root/index.html +++ b/k8s/document_root/index.html @@ -226,7 +226,7 @@ dig @ns.sslip.io txt ip.sslip.io +short -6 # forces IPv6 lookup; sample reply "2 ns-aws.sslip.io requires a mere 592 bytes spread over 2 packets; Querying https://icanhazip.com/ requires 8692 bytes spread out over 34 packets—over 14 times as much! Admittedly bandwidth usage is a bigger concern for the one hosting the service than the one using the - service.

+ service.

Determining The Server Version of Software

You can determine the server version of the - sslip.io software by querying the TXT record of version.sslip.io: + sslip.io software by querying the TXT record of version.status.sslip.io:
-dig @ns-aws.sslip.io txt version.sslip.io +short
+dig @ns-aws.sslip.io txt version.status.sslip.io +short
             "2.2.3"
             "2021/11/27-11:35:50-0800"
             "074f0a8"
@@ -271,10 +272,28 @@ dig @ns-aws.sslip.io txt version.sslip.io +short
       

The first number, ("2.2.3"), is the version of the sslip.io DNS software, and is most relevant. The other two numbers are the date compiled and the most recent git hash, but those values can differ across servers due to the manner in which the software is deployed.

+

Server Metrics

You can retrieve metrics from a given server by querying the TXT records of + metrics.status.sslip.io +
+dig @ns-azure.sslip.io txt version.status.sslip.io +short
+            "uptime (seconds): 1200"
+            "key-value store: builtin"
+            "queries: 46202"
+            "queries/second: 38.5"
+            "successful:"
+            "- queries/second: 14.5"
+            "- A: 2000"
+            "- AAAA: 20"
+            "- IP TXT: 2"
+            "- version TXT: 2"
+            "- DNS-01 challenge: 2"
+