From bcceeba8586dee841dc2bbdd1ee8e335887c70a2 Mon Sep 17 00:00:00 2001 From: Brian Cunnie Date: Sat, 22 Jan 2022 15:51:35 -0800 Subject: [PATCH] Mitigate DNS amplification attack surface `metrics.status.sslip.io` is a vector for a DNS amplification attack; we mitigate it by latching a 1/4 second throttle on each query after a certain amount of queries. That endpoint is a 4x amplifier: 100byte request with a 400 byte reply. --- .../sslip.io-dns-server/integration_test.go | 23 +++++++++++++ bosh-release/src/sslip.io-dns-server/main.go | 32 ++++++++++++++++++- .../src/sslip.io-dns-server/xip/xip.go | 10 ++++-- 3 files changed, 61 insertions(+), 4 deletions(-) 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 60a7ba4..7cdf18c 100644 --- a/bosh-release/src/sslip.io-dns-server/integration_test.go +++ b/bosh-release/src/sslip.io-dns-server/integration_test.go @@ -4,6 +4,7 @@ import ( "os/exec" "strings" "time" + "xip/xip" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -297,5 +298,27 @@ var _ = Describe("sslip.io-dns-server", func() { }) }) }) + When(`a TXT record for an "metrics.status.sslip.io" domain is repeatedly queries`, func() { + It("rate-limits the queries after some amount requests", func() { + // typically ~9 milliseconds / query, ~125 queries / sec on 4-core Xeon + var start, stop time.Time + throttled := false + // add an extra ten to the loop to really make sure we've exhausted the buffered channel + for i := 0; i < xip.MetricsBufferSize+10; i += 1 { + start = time.Now() + digArgs = "@localhost metrics.status.sslip.io txt" + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + _, err := digCmd.Output() + Expect(err).ToNot(HaveOccurred()) + stop = time.Now() + // we currently buffer at 250 milliseconds, so for our test we use a smidgen less because jitter + if stop.Sub(start) > 240*time.Millisecond { + throttled = true + break + } + } + Expect(throttled).To(BeTrue()) + }) + }) }) }) diff --git a/bosh-release/src/sslip.io-dns-server/main.go b/bosh-release/src/sslip.io-dns-server/main.go index ca6271d..3d27643 100644 --- a/bosh-release/src/sslip.io-dns-server/main.go +++ b/bosh-release/src/sslip.io-dns-server/main.go @@ -81,6 +81,31 @@ func main() { func readFrom(conn *net.UDPConn, wg *sync.WaitGroup, etcdCli xip.V3client, xipMetrics *xip.Metrics) { defer wg.Done() + // We want to make sure that our DNS server isn't used in a DNS amplification attack. + // The endpoint we're worried about is metrics.status.sslip.io, whose reply is + // ~400 bytes with a query of ~100 bytes (4x amplification). We accomplish this by + // using channels with a quarter-second delay. Max throughput 1.2 kBytes/sec. + // + // We want to balance this delay against our desire to run tests quickly, so we buffer + // the channel with enough room to accommodate our tests. + // + // We realize that, if we're listening on several network interfaces, we're throttling + // _per interface_, not from a global standpoint, but we didn't want to clutter + // main() more than necessary. + // + // We also want to have fun playing with channels + dnsAmplificationAttackDelay := make(chan struct{}, xip.MetricsBufferSize) + go func() { + // fill up the channel's buffer so that our tests aren't slowed down (~85 tests) + for i := 0; i < xip.MetricsBufferSize; i += 1 { + dnsAmplificationAttackDelay <- struct{}{} + } + // now put on the brakes for users trying to leverage our server in a DNS amplification attack + for { + dnsAmplificationAttackDelay <- struct{}{} + time.Sleep(250 * time.Millisecond) + } + }() for { query := make([]byte, 512) _, addr, err := conn.ReadFromUDP(query) @@ -89,7 +114,12 @@ func readFrom(conn *net.UDPConn, wg *sync.WaitGroup, etcdCli xip.V3client, xipMe continue } go func() { - xipServer := xip.Xip{SrcAddr: addr.IP, Etcd: etcdCli, Metrics: xipMetrics} + xipServer := xip.Xip{ + SrcAddr: addr.IP, + Etcd: etcdCli, + Metrics: xipMetrics, + DnsAmplificationAttackDelay: dnsAmplificationAttackDelay, + } response, logMessage, err := xipServer.QueryResponse(query) if err != nil { log.Println(err.Error()) 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 903368b..318f236 100644 --- a/bosh-release/src/sslip.io-dns-server/xip/xip.go +++ b/bosh-release/src/sslip.io-dns-server/xip/xip.go @@ -32,9 +32,10 @@ type V3client interface { // through the call hierarchy // (the source address for `ip.sslip.io`, and the etcd client for `k-v.io`) type Xip struct { - SrcAddr net.IP - Etcd V3client - Metrics *Metrics + SrcAddr net.IP + Etcd V3client + DnsAmplificationAttackDelay chan struct{} + Metrics *Metrics } type Metrics struct { @@ -109,6 +110,8 @@ var ( VersionDate = "0001/01/01-99:99:99-0800" VersionGitHash = "cafexxx" + MetricsBufferSize = 100 + TxtKvCustomizations = KvCustomizations{} Customizations = DomainCustomizations{ "sslip.io.": { @@ -793,6 +796,7 @@ func ipSslipIo(x Xip) ([]dnsmessage.TXTResource, error) { // when TXT for "metrics.sslip.io" is queried, return the cumulative metrics func metricsSslipIo(x Xip) (txtResources []dnsmessage.TXTResource, err error) { + <-x.DnsAmplificationAttackDelay var metrics []string uptime := time.Since(x.Metrics.Start) metrics = append(metrics, fmt.Sprintf("uptime (seconds): %.0f", uptime.Seconds()))