From ca19f02369a55596a877cf9071cbd23a8aec7d0d Mon Sep 17 00:00:00 2001 From: Brian Cunnie Date: Sun, 24 Aug 2025 14:11:58 -0700 Subject: [PATCH] Performance-tune the blocklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously we blocked by CIDRs, not IPs, but that was flawed: of the 746 CIDRs, 744 of them were /32 — in other words, IP addresses. And matching CIDRs is computationally expensive: consuming 4.8% of the CPU for each query. We switched to a string-indexed map instead to accelerate matching. - Fivefold increase in blocklist lookup speed, dropping from consuming 4.8% of the CPU to 0.96% - Added a new member, `xip.BlocklistIPs` - All blocked sites are IPv4. I have never gotten a takedown for an IPv6 site - I wanted to maintain backwards-compatiblity with my blocklist file; I didn't want to be forced to coordinate updating that simultaneously with a deploy of this code, hence the automated "/32" conversion from a CIDR to an IP address - I cleaned up the test blocklist file (`blocklist-test.txt`); it's easier to read & understand - I added profiling from before, `profile/cpu-cidr.prof`, and after, `profile/cpu-ip.prof`, the change. --- etc/blocklist-test.txt | 9 ++-- integration_test.go | 16 ++++-- k8s/document_root_sslip.io/index.html | 12 ++--- profile/cpu-cidr.prof | Bin 0 -> 16730 bytes profile/cpu-ip.prof | Bin 0 -> 24050 bytes xip/xip.go | 72 +++++++++++++++++++------- xip/xip_test.go | 58 ++++++++++++++------- 7 files changed, 114 insertions(+), 53 deletions(-) create mode 100644 profile/cpu-cidr.prof create mode 100644 profile/cpu-ip.prof diff --git a/etc/blocklist-test.txt b/etc/blocklist-test.txt index 56d2a4c..218401f 100644 --- a/etc/blocklist-test.txt +++ b/etc/blocklist-test.txt @@ -3,8 +3,7 @@ # This is a shortened variant meant to be used for testing (`ginkgo`) because # the legitimate one has grown so long it clutters the test output -raiffeisen # https://www.rbinternational.com/en/homepage.html -43-134-66-67 # Netflix, https://nf-43-134-66-67.sslip.io/sg -43.134.66.67/24 # Netflix -2601:646:100:69f7:cafe:bebe:cafe:bebe/112 # personal (Comcast) IPv6 range for testing blocklist - +12.34.56.78/24 # IPv4 CIDR +1234::/64 # IPv6 CIDR +23.45.67.89 # IPv4 +raiffeisen # string diff --git a/integration_test.go b/integration_test.go index d029b85..19aed75 100644 --- a/integration_test.go +++ b/integration_test.go @@ -452,13 +452,21 @@ var _ = Describe("sslip.io-dns-server", func() { `\Ans-[a-z-]+.sslip.io.\nns-[a-z-]+.sslip.io.\nns-[a-z-]+.sslip.io.\nns-[a-z-]+.sslip.io.\n\z`, `TypeNS _acme-challenge.raiffeisen.fe80--.sslip.io. \? ns-do-sg.sslip.io., ns-gce.sslip.io., ns-hetzner.sslip.io., ns-ovh.sslip.io.\n$`), Entry("an A record with a forbidden CIDR is redirected", - "@localhost nf.43.134.66.67.sslip.io +short", + "@localhost nf.12.34.56.0.sslip.io +short", `\A52.0.56.137\n\z`, - `TypeA nf.43.134.66.67.sslip.io. \? 52.0.56.137\n$`), + `TypeA nf.12.34.56.0.sslip.io. \? 52.0.56.137\n$`), + Entry("an A record with a forbidden IP is redirected", + "@localhost nf.23.45.67.89.sslip.io +short", + `\A52.0.56.137\n\z`, + `TypeA nf.23.45.67.89.sslip.io. \? 52.0.56.137\n$`), + Entry("an A record with a forbidden IP with dashes is redirected", + "@localhost nf.23-45-67-89.sslip.io +short", + `\A52.0.56.137\n\z`, + `TypeA nf.23-45-67-89.sslip.io. \? 52.0.56.137\n$`), Entry("an AAAA record with a forbidden CIDR is redirected", - "@localhost 2601-646-100-69f7-cafe-bebe-cafe-baba.sslip.io aaaa +short", + "@localhost 1234--1.sslip.io aaaa +short", `\A2600:1f18:aaf:6900::a\n\z`, - `TypeAAAA 2601-646-100-69f7-cafe-bebe-cafe-baba.sslip.io. \? 2600:1f18:aaf:6900::a\n$`), + `TypeAAAA 1234--1.sslip.io. \? 2600:1f18:aaf:6900::a\n$`), ) }) When("it can't bind to any UDP port", func() { diff --git a/k8s/document_root_sslip.io/index.html b/k8s/document_root_sslip.io/index.html index 78c46a3..fe9603b 100644 --- a/k8s/document_root_sslip.io/index.html +++ b/k8s/document_root_sslip.io/index.html @@ -232,7 +232,7 @@ dig @ns-ovh.nip.io version.status.nip.io txt +short
   dig @ns-ovh.nip.io metrics.status.nip.io txt +short
     "Uptime: 1168705"
-    "Blocklist: 2025-07-22 04:30:18-07 3,722"
+    "Blocklist: 2025-07-22 04:30:18-07 3,722,2"
     "Queries: 2619971786 (2241.8/s)"
     "TCP/UDP: 2949450/2617181176"
     "Answer > 0: 934226491 (799.4/s)"
@@ -250,11 +250,11 @@ dig @ns-ovh.nip.io version.status.nip.io txt +short
         
The time since the DNS server has been started, in seconds
Blocklist
- The first value ("2023-10-04 07:37:50-07") is the date the blocklist was last downloaded. The following two - numbers are the number of string matches that are blocked (e.g. "raiffeisen" is a string that is blocked if - it appears in the queried hostname) and the number of CIDR matches that are blocked (e.g. "43.134.66.67/24" - is blocked). The blocklist can be found here + The first value ("2023-10-04 07:37:50-07") is the date the blocklist was last downloaded. The following three + numbers are the number of CIDR matches that are blocked (e.g. 86.106.104.0/24), the number of IP addresses + that are blocked (e.g. 212.64.214.54), and the number of strings that are blocked (e.g. "raiffeisen" is a + string that is blocked if it appears in the queried hostname). The blocklist can be found here
Queries
This consists of two numbers: The first is the raw number of DNS queries that the server has responded to diff --git a/profile/cpu-cidr.prof b/profile/cpu-cidr.prof new file mode 100644 index 0000000000000000000000000000000000000000..2f601625433a2189af744748415b8dd00268a679 GIT binary patch literal 16730 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 literal 0 HcmV?d00001 diff --git a/xip/xip.go b/xip/xip.go index e5cf42f..17495e6 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -28,8 +28,9 @@ import ( type Xip struct { DnsAmplificationAttackDelay chan struct{} // for throttling metrics.status.sslip.io Metrics Metrics // DNS server metrics - BlocklistStrings []string // list of blacklisted strings that shouldn't appear in public hostnames - BlocklistCIDRs []net.IPNet // list of blacklisted CIDRs; no A/AAAA records should resolve to IPs in these CIDRs + BlocklistCIDRs []net.IPNet // list of blocked CIDRs; no A/AAAA records should resolve to IPs in these CIDRs + BlocklistIPs map[string]struct{} // list of blocked IPs; no A/AAAA records should resolve to these IPs + BlocklistStrings []string // list of blocked strings that shouldn't appear in public hostnames BlocklistUpdated time.Time // The most recent time the Blocklist was updated NameServers []dnsmessage.NSResource // The list of authoritative name servers (NS) Public bool // Whether to resolve public IPs; set to false if security-conscious @@ -1076,10 +1077,12 @@ func TXTMetrics(x *Xip, _ net.IP) (txtResources []dnsmessage.TXTResource, err er var metrics []string uptime := time.Since(x.Metrics.Start) metrics = append(metrics, fmt.Sprintf("Uptime: %.0f", uptime.Seconds())) - metrics = append(metrics, fmt.Sprintf("Blocklist: %s %d,%d", + metrics = append(metrics, fmt.Sprintf("Blocklist: %s %d,%d,%d", x.BlocklistUpdated.Format("2006-01-02 15:04:05-07"), + len(x.BlocklistCIDRs), + len(x.BlocklistIPs), len(x.BlocklistStrings), - len(x.BlocklistCIDRs))) + )) metrics = append(metrics, fmt.Sprintf("Queries: %d (%.1f/s)", x.Metrics.Queries, float64(x.Metrics.Queries)/uptime.Seconds())) metrics = append(metrics, fmt.Sprintf("TCP/UDP: %d/%d", x.Metrics.TCPQueries, x.Metrics.UDPQueries)) metrics = append(metrics, fmt.Sprintf("Answer > 0: %d (%.1f/s)", x.Metrics.AnsweredQueries, float64(x.Metrics.AnsweredQueries)/uptime.Seconds())) @@ -1152,21 +1155,25 @@ func (x *Xip) downloadBlockList(blocklistURL string) string { return fmt.Sprintf(`failed to download blocklist "%s", HTTP status: "%d"`, blocklistURL, resp.StatusCode) } } - blocklistStrings, blocklistCIDRs, err := ReadBlocklist(blocklistReader) + blocklistCIDRs, blocklistIPs, blocklistStrings, err := ReadBlocklist(blocklistReader) if err != nil { return fmt.Sprintf(`failed to parse blocklist "%s": %s`, blocklistURL, err.Error()) } - x.BlocklistStrings = blocklistStrings x.BlocklistCIDRs = blocklistCIDRs + x.BlocklistIPs = blocklistIPs + x.BlocklistStrings = blocklistStrings x.BlocklistUpdated = time.Now() - return fmt.Sprintf("Successfully downloaded blocklist from %s: %v, %v", blocklistURL, x.BlocklistStrings, x.BlocklistCIDRs) + return fmt.Sprintf("Successfully downloaded blocklist from %s: %v, %v, %v", blocklistURL, x.BlocklistCIDRs, x.BlocklistIPs, x.BlocklistStrings) } // ReadBlocklist "sanitizes" the block list, removing comments, invalid characters // and lowercasing the names to be blocked. // public to make testing easier -func ReadBlocklist(blocklist io.Reader) (stringBlocklists []string, cidrBlocklists []net.IPNet, err error) { +func ReadBlocklist(blocklist io.Reader) (blocklistCIDRs []net.IPNet, blocklistIPs map[string]struct{}, blocklistStrings []string, err error) { scanner := bufio.NewScanner(blocklist) + blocklistCIDRs = []net.IPNet{} + blocklistIPs = make(map[string]struct{}) + blocklistStrings = []string{} comments := regexp.MustCompile(`#.*`) invalidDNSchars := regexp.MustCompile(`[^-\da-z]`) invalidDNScharsWithSlashesDotsAndColons := regexp.MustCompile(`[^-_\da-z/.:]`) @@ -1177,20 +1184,42 @@ func ReadBlocklist(blocklist io.Reader) (stringBlocklists []string, cidrBlocklis line = comments.ReplaceAllString(line, "") // strip comments line = invalidDNScharsWithSlashesDotsAndColons.ReplaceAllString(line, "") // strip invalid characters _, ipcidr, err := net.ParseCIDR(line) - if err != nil { - line = invalidDNSchars.ReplaceAllString(line, "") // strip invalid DNS characters - if line == "" { + if err == nil { + // Previously we blocked by CIDRs, not IPs, but that was flawed: + // of the 746 CIDRs, 744 of them were /32 — in other words, IP + // addresses. And matching CIDRs is computationally expensive: + // consuming 0.25s of 2.21s of xip.QueryResponse() -> 11%, + // so we use a string-indexed map instead + // + // All blocked sites are IPv4. I have never gotten a takedown for an IPv6 + if ipcidr.IP.To4() != nil && ipcidr.Mask.String() == "ffffffff" { + blocklistIPs[ipcidr.IP.String()] = struct{}{} continue } - stringBlocklists = append(stringBlocklists, line) - } else { - cidrBlocklists = append(cidrBlocklists, *ipcidr) + // We still need CIDRs though, especially for poorly-secured WordPress + // hosting sites like Valkyrie Hosting, where we block entire subnets + blocklistCIDRs = append(blocklistCIDRs, *ipcidr) + continue } + // it's not a CIDR; is it an IP? + // we convert the IP to a string because we can't use net.IP as a map index + ip := net.ParseIP(line) + if ip != nil { + blocklistIPs[ip.String()] = struct{}{} + continue + } + // it's not a CIDR or IP; is it a string? + line = invalidDNSchars.ReplaceAllString(line, "") // strip [/.:] + if line == "" { + continue + } + // it's a string + blocklistStrings = append(blocklistStrings, line) } if err = scanner.Err(); err != nil { - return []string{}, []net.IPNet{}, err + return []net.IPNet{}, map[string]struct{}{}, []string{}, err } - return stringBlocklists, cidrBlocklists, nil + return blocklistCIDRs, blocklistIPs, blocklistStrings, nil } func (x *Xip) blocklist(hostname string) bool { @@ -1212,13 +1241,16 @@ func (x *Xip) blocklist(hostname string) bool { if ip.IsPrivate() { return false } - for _, blockstring := range x.BlocklistStrings { - if strings.Contains(hostname, blockstring) { + for _, blockCIDR := range x.BlocklistCIDRs { + if blockCIDR.Contains(ip) { return true } } - for _, blockCIDR := range x.BlocklistCIDRs { - if blockCIDR.Contains(ip) { + if _, exists := x.BlocklistIPs[ip.String()]; exists { + return true + } + for _, blockstring := range x.BlocklistStrings { + if strings.Contains(hostname, blockstring) { return true } } diff --git a/xip/xip_test.go b/xip/xip_test.go index 25c1416..c237d17 100644 --- a/xip/xip_test.go +++ b/xip/xip_test.go @@ -514,47 +514,69 @@ var _ = Describe("Xip", func() { Describe("ReadBlocklist()", func() { It("strips comments", func() { input := strings.NewReader("# a comment\n#another comment\nno-comments\n") - bls, blIPs, err := xip.ReadBlocklist(input) + blCIDRs, blIPs, blStrings, err := xip.ReadBlocklist(input) Expect(err).ToNot(HaveOccurred()) - Expect(bls).To(Equal([]string{"no-comments"})) - Expect(blIPs).To(BeNil()) + Expect(len(blCIDRs)).To(BeZero()) + Expect(blIPs).To(Equal(map[string]struct{}{})) + Expect(blStrings).To(Equal([]string{"no-comments"})) }) It("strips blank lines", func() { input := strings.NewReader("\n\n\nno-blank-lines") - bls, blIPs, err := xip.ReadBlocklist(input) + blCIDRs, blIPs, blStrings, err := xip.ReadBlocklist(input) Expect(err).ToNot(HaveOccurred()) - Expect(bls).To(Equal([]string{"no-blank-lines"})) - Expect(blIPs).To(BeNil()) + Expect(len(blCIDRs)).To(BeZero()) + Expect(blIPs).To(Equal(map[string]struct{}{})) + Expect(blStrings).To(Equal([]string{"no-blank-lines"})) }) It("lowercases names for comparison", func() { input := strings.NewReader("NO-YELLING") - bls, blIPs, err := xip.ReadBlocklist(input) + blCIDRs, blIPs, blStrings, err := xip.ReadBlocklist(input) Expect(err).ToNot(HaveOccurred()) - Expect(bls).To(Equal([]string{"no-yelling"})) - Expect(blIPs).To(BeNil()) + Expect(len(blCIDRs)).To(BeZero()) + Expect(blIPs).To(Equal(map[string]struct{}{})) + Expect(blStrings).To(Equal([]string{"no-yelling"})) }) It("removes all non-allowable characters", func() { input := strings.NewReader("\nalpha #comment # comment\nåß∂ # comment # comment\ndelta∆\n ... GAMMA∑µ®† ...#asdfasdf#asdfasdf") - bls, blIPs, err := xip.ReadBlocklist(input) + blCIDRs, blIPs, blStrings, err := xip.ReadBlocklist(input) Expect(err).ToNot(HaveOccurred()) - Expect(bls).To(Equal([]string{"alpha", "delta", "gamma"})) - Expect(blIPs).To(BeNil()) + Expect(len(blCIDRs)).To(BeZero()) + Expect(blIPs).To(Equal(map[string]struct{}{})) + Expect(blStrings).To(Equal([]string{"alpha", "delta", "gamma"})) }) It("reads in IPv4 CIDRs", func() { input := strings.NewReader("\n43.134.66.67/24 #asdfasdf") - bls, blIPs, err := xip.ReadBlocklist(input) + blCIDRs, blIPs, blStrings, err := xip.ReadBlocklist(input) Expect(err).ToNot(HaveOccurred()) - Expect(bls).To(BeNil()) - Expect(blIPs).To(Equal([]net.IPNet{{IP: net.IP{43, 134, 66, 0}, Mask: net.IPMask{255, 255, 255, 0}}})) + Expect(blCIDRs).To(Equal([]net.IPNet{{IP: net.IP{43, 134, 66, 0}, Mask: net.IPMask{255, 255, 255, 0}}})) + Expect(blIPs).To(Equal(map[string]struct{}{})) + Expect(len(blStrings)).To(BeZero()) + }) + It("reads in IPv4 CIDRs, but with a /32 converts it to an IP address", func() { + input := strings.NewReader("\n43.134.66.67/32 #asdfasdf") + blCIDRs, blIPs, blStrings, err := xip.ReadBlocklist(input) + Expect(err).ToNot(HaveOccurred()) + Expect(len(blCIDRs)).To(BeZero()) + Expect(blIPs).To(Equal(map[string]struct{}{"43.134.66.67": {}})) + Expect(len(blStrings)).To(BeZero()) }) It("reads in IPv6 CIDRs", func() { input := strings.NewReader("\n 2600::/64 #asdfasdf") - bls, blIPs, err := xip.ReadBlocklist(input) + blCIDRs, blIPs, blStrings, err := xip.ReadBlocklist(input) Expect(err).ToNot(HaveOccurred()) - Expect(bls).To(BeNil()) - Expect(blIPs).To(Equal([]net.IPNet{ + Expect(blCIDRs).To(Equal([]net.IPNet{ {IP: net.IP{38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0}}})) + Expect(blIPs).To(Equal(map[string]struct{}{})) + Expect(len(blStrings)).To(BeZero()) + }) + It("reads in IPv4 IP addresses (but not IPv6)", func() { + input := strings.NewReader("\n 104.155.144.4 #asdfasdf") + blCIDRs, blIPs, blStrings, err := xip.ReadBlocklist(input) + Expect(err).ToNot(HaveOccurred()) + Expect(len(blCIDRs)).To(BeZero()) + Expect(blIPs).To(Equal(map[string]struct{}{"104.155.144.4": {}})) + Expect(len(blStrings)).To(BeZero()) }) })