mirror of
https://github.com/cunnie/sslip.io.git
synced 2025-10-05 15:46:50 +08:00

- located in Warsaw, Poland - IPv4: 51.75.53.19 - IPv6: 2001:41d0:602:2313::1 The crux of this is to take the load off ns-aws, which jumped from $12.66 → $20.63 → $38.51 → $62.30 in the last four months due to bandwidth charges exceeding 10 TB. The real fix is to randomize the order in which the nameservers are returned.
623 lines
31 KiB
Go
623 lines
31 KiB
Go
package main_test
|
|
|
|
import (
|
|
"log"
|
|
"net"
|
|
"os/exec"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"xip/testhelper"
|
|
"xip/xip"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
. "github.com/onsi/gomega/gbytes"
|
|
. "github.com/onsi/gomega/gexec"
|
|
)
|
|
|
|
var err error
|
|
var serverCmd *exec.Cmd
|
|
var serverSession *Session
|
|
var port = getFreePort()
|
|
var serverPath, _ = Build("main.go")
|
|
|
|
var _ = BeforeSuite(func() {
|
|
Expect(err).ToNot(HaveOccurred())
|
|
serverCmd = exec.Command(serverPath, "-port", strconv.Itoa(port), "-blocklistURL", "file://etc/blocklist-test.txt")
|
|
serverSession, err = Start(serverCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
// takes 0.455s to start up on macOS Big Sur 3.7 GHz Quad Core 22-nm Xeon E5-1620v2 processor (2013 Mac Pro)
|
|
// takes 1.312s to start up on macOS Big Sur 2.0GHz quad-core 10th-generation Intel Core i5 processor (2020 13" MacBook Pro)
|
|
// 10 seconds should be long enough for slow container-on-a-VM-with-shared-core
|
|
Eventually(serverSession.Err, 10).Should(Say(` version \d+\.\d+\.\d+ starting`))
|
|
Eventually(serverSession.Err, 10).Should(Say("Ready to answer queries"))
|
|
})
|
|
|
|
var _ = AfterSuite(func() {
|
|
serverSession.Terminate()
|
|
Eventually(serverSession).Should(Exit())
|
|
})
|
|
|
|
var _ = Describe("sslip.io-dns-server", func() {
|
|
//var stdin io.WriteCloser
|
|
var digCmd *exec.Cmd
|
|
var digSession *Session
|
|
var digArgs string
|
|
|
|
Describe("Integration tests", func() {
|
|
DescribeTable("when the DNS server is queried",
|
|
func(digArgs string, digResults string, serverLogMessage string) {
|
|
digArgs += " -p " + strconv.Itoa(port)
|
|
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 compare the _full_ contents of the stdout in the case
|
|
// of negative assertions (e.g. "^$")
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(digResults))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(serverLogMessage))
|
|
},
|
|
Entry("A (customized) for sslip.io",
|
|
"@localhost sslip.io +short",
|
|
`\A78.46.204.247\n\z`,
|
|
`TypeA sslip.io. \? 78.46.204.247\n`),
|
|
Entry("A (or lack thereof) for example.com",
|
|
"@localhost example.com +short",
|
|
`\A\z`,
|
|
`TypeA example.com. \? nil, SOA example.com. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry("A for www-127-0-0-1.sslip.io",
|
|
"@localhost www-127-0-0-1.sslip.io +short",
|
|
`\A127.0.0.1\n\z`,
|
|
`TypeA www-127-0-0-1.sslip.io. \? 127.0.0.1\n`),
|
|
Entry("A for www.192.168.0.1.sslip.io",
|
|
"@localhost www.192.168.0.1.sslip.io +short",
|
|
`\A192.168.0.1\n\z`,
|
|
`TypeA www.192.168.0.1.sslip.io. \? 192.168.0.1\n`),
|
|
Entry("AAAA (customized) for sslip.io",
|
|
"@localhost sslip.io aaaa +short",
|
|
`\A2a01:4f8:c17:b8f::2\n\z`,
|
|
`TypeAAAA sslip.io. \? 2a01:4f8:c17:b8f::2\n`),
|
|
Entry("AAAA not found for example.com",
|
|
"@localhost example.com aaaa +short",
|
|
`\A\z`,
|
|
`TypeAAAA example.com. \? nil, SOA example.com. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry("AAAA for www-2601-646-100-69f0-1c09-bae7-aa42-146c.sslip.io",
|
|
"@localhost www-2601-646-100-69f0-1c09-bae7-aa42-146c.sslip.io aaaa +short",
|
|
`\A2601:646:100:69f0:1c09:bae7:aa42:146c\n\z`,
|
|
`TypeAAAA www-2601-646-100-69f0-1c09-bae7-aa42-146c.sslip.io. \? 2601:646:100:69f0:1c09:bae7:aa42:146c\n`),
|
|
Entry("ALL (ANY) is NOT implemented",
|
|
// `+notcp` required for dig 9.11.25-RedHat-9.11.25-2.fc32 to avoid "connection refused"
|
|
"@localhost sslip.io any +notcp",
|
|
` status: NOTIMP,`,
|
|
`TypeALL sslip.io. \? NotImplemented\n`),
|
|
Entry("CNAME (customized) for protonmail._domainkey.sslip.io",
|
|
"@localhost protonmail._domainkey.sslip.io cname +short",
|
|
`\Aprotonmail.domainkey.dw4gykv5i2brtkjglrf34wf6kbxpa5hgtmg2xqopinhgxn5axo73a.domains.proton.ch.\n\z`,
|
|
`TypeCNAME protonmail._domainkey.sslip.io. \? protonmail.domainkey.dw4gykv5i2brtkjglrf34wf6kbxpa5hgtmg2xqopinhgxn5axo73a.domains.proton.ch.\n`),
|
|
Entry("CNAME not found for example.com",
|
|
"@localhost example.com cname +short",
|
|
`\A\z`,
|
|
`TypeCNAME example.com. \? nil, SOA example.com. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry("MX for example.com",
|
|
"@localhost example.com mx +short",
|
|
`\A0 example.com.\n\z`,
|
|
`TypeMX example.com. \? 0 example.com.\n`),
|
|
Entry("SOA for sslip.io",
|
|
"@localhost sslip.io soa +short",
|
|
`\Asslip.io. briancunnie.gmail.com. 2024091500 900 900 1800 180\n\z`,
|
|
`TypeSOA sslip.io. \? sslip.io. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry("SOA for example.com",
|
|
"@localhost example.com soa +short",
|
|
`\Aexample.com. briancunnie.gmail.com. 2024091500 900 900 1800 180\n\z`,
|
|
`TypeSOA example.com. \? example.com. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry("SRV (or other record that we don't implement) for example.com",
|
|
"@localhost example.com srv +short",
|
|
`\A\z`,
|
|
`TypeSRV example.com. \? nil, SOA example.com. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
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"0.0.0"\n"0001/01/01-99:99:99-0800"\n"cafexxx"\n\z`,
|
|
`TypeTXT version.status.sslip.io. \? \["0.0.0"\], \["0001/01/01-99:99:99-0800"\], \["cafexxx"\]`),
|
|
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`,
|
|
`TypeTXT ip.sslip.io. \? \["127.0.0.1"\]`),
|
|
Entry(`TXT is the querier's IPv4 address and the domain is NOT "ip.sslip.io"`,
|
|
"@127.0.0.1 example.com txt +short",
|
|
`\A\z`,
|
|
`TypeTXT example.com. \? nil, SOA example.com. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry(`get a PTR for 1.0.168.192.in-addr.arpa returns 192-168-0-1.sslip.io`,
|
|
"@127.0.0.1 ptr -x 192.168.0.1 +short",
|
|
`\A192-168-0-1.sslip.io.\n\z`,
|
|
`TypePTR 1.0.168.192.in-addr.arpa. \? 192-168-0-1.sslip.io.`),
|
|
Entry(`get a PTR for 1.0.0.127.blah.in-addr.arpa returns no records; "blah.in-addr.arpa is not a valid domain."`,
|
|
"@127.0.0.1 1.0.0.127.blah.in-addr.arpa ptr +short",
|
|
`\A\z`,
|
|
`TypePTR 1.0.0.127.blah.in-addr.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry(`get a PTR for blah.1.0.0.127.in-addr.arpa returns no records; "blah" isn't a valid subdomain' `,
|
|
"@127.0.0.1 blah.1.0.0.127.in-addr.arpa ptr +short",
|
|
`\A\z`,
|
|
`TypePTR blah.1.0.0.127.in-addr.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry(`get a PTR for 0.0.127.in-addr.arpa returns no records; should have 4 octets, not 3`,
|
|
"@127.0.0.1 0.0.127.in-addr.arpa ptr +short",
|
|
`\A\z`,
|
|
`TypePTR 0.0.127.in-addr.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry(`get a PTR for 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa returns 2601-646-100-69f0-14ce-6eea-9204-bba2.sslip.io`,
|
|
"@127.0.0.1 ptr -x 2601:646:100:69f0:14ce:6eea:9204:bba2 +short",
|
|
`\A2601-646-100-69f0-14ce-6eea-9204-bba2.sslip.io.\n\z`,
|
|
`TypePTR 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa. \? 2601-646-100-69f0-14ce-6eea-9204-bba2.sslip.io.`),
|
|
Entry(`get a PTR for 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.blah.ip6.arpa returns no records; "blah isn't a valid subdomain'"`,
|
|
"@127.0.0.1 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.blah.ip6.arpa ptr +short",
|
|
`\A\z`,
|
|
`TypePTR 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.blah.ip6.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry(`get a PTR for b2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa returns no records; "b2" isn't a valid subdomain'`,
|
|
"@127.0.0.1 b2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa ptr +short",
|
|
`\A\z`,
|
|
`TypePTR b2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry(`get a PTR for b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa returns no records; has too few numbers`,
|
|
"@127.0.0.1 b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa ptr +short",
|
|
`\A\z`,
|
|
`TypePTR b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 2024091500 900 900 1800 180\n`),
|
|
Entry(`TODO: should, but doesn't, return an IDNA2008-compliant record for ::1`,
|
|
"@127.0.0.1 -x ::1 +short",
|
|
`\A--1.sslip.io.\n\z`,
|
|
`TypePTR 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. \? --1.sslip.io.\n`),
|
|
Entry(`TODO: should, but doesn't, return an IDNA2008-compliant record for 2600::`,
|
|
"@127.0.0.1 -x 2600:: +short",
|
|
`\A2600--.sslip.io.\n\z`,
|
|
`TypePTR 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.2.ip6.arpa. \? 2600--.sslip.io.\n`),
|
|
Entry(`over TCP, A (customized) for sslip.io`,
|
|
"@localhost sslip.io +short +vc",
|
|
`\A78.46.204.247\n\z`,
|
|
`TypeA sslip.io. \? 78.46.204.247\n`),
|
|
)
|
|
})
|
|
Describe("for more complex assertions", func() {
|
|
When("we want to make sure our TTL is an hour if we need to block ", func() {
|
|
It("returns a TTL of 3600, at least for the non-RFC 1918 non-localhost IPv4 adresses", func() {
|
|
digArgs = "@localhost 52.0.56.138.sslip.io -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`52\.0\.56\.138\.sslip\.io\.\s+3600\s+IN\s+A\s+52\.0\.56\.138\n`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA 52\.0\.56\.138\.sslip\.io\. \? 52\.0\.56\.138\n`))
|
|
})
|
|
It("returns a TTL of 3600, at least for the non-RFC 4193 non-localhost IPv6 addresses", func() {
|
|
digArgs = "@localhost aaaa 2600-1f18-aaf-6900--b.sslip.io -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`2600-1f18-aaf-6900--b.sslip.io.\s+3600\s+IN\s+AAAA\s+2600:1f18:aaf:6900::b`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeAAAA 2600-1f18-aaf-6900--b\.sslip\.io\. \? 2600:1f18:aaf:6900::b\n`))
|
|
})
|
|
})
|
|
When("our test is run on a machine which has IPv6", func() {
|
|
cmd := exec.Command("ping6", "-c", "1", "::1")
|
|
err := cmd.Run() // if the command succeeds, we have IPv6
|
|
if err == nil {
|
|
It("returns a TXT of the querier's IPv6 address when querying ip.sslip.io", func() {
|
|
digCmd = exec.Command("dig", "@::1", "ip.sslip.io", "txt", "+short", "-p", strconv.Itoa(port))
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(`::1`))
|
|
Eventually(serverSession.Err).Should(Say(`TypeTXT ip\.sslip\.io\. \? \["::1"\]`))
|
|
Expect(digSession).To(Exit())
|
|
})
|
|
}
|
|
})
|
|
When("we do reverse lookups (PTR) on a random series of IPv6 addresses (fuzz testing)", func() {
|
|
It("should succeed every time", func() {
|
|
for i := 0; i < 50; i++ {
|
|
addr := testhelper.RandomIPv6Address()
|
|
digArgs = "@localhost -x " + addr.String() + " -p " + strconv.Itoa(port) + " +short"
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
expectedPtr := strings.ReplaceAll(addr.String(), ":", "-") + ".sslip.io."
|
|
Eventually(digSession).Should(Say(expectedPtr))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
}
|
|
})
|
|
})
|
|
When("ns.sslip.io is queried", func() {
|
|
It("returns all the A records", func() {
|
|
digArgs = "@localhost ns.sslip.io +short -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`52.0.56.137`))
|
|
Eventually(digSession).Should(Say(`52.187.42.158`))
|
|
Eventually(digSession).Should(Say(`104.155.144.4`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA ns.sslip.io. \? 52.0.56.137, 52.187.42.158, 104.155.144.4\n`))
|
|
})
|
|
It("returns all the AAAA records", func() {
|
|
digArgs = "@localhost aaaa ns.sslip.io +short -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`2600:1f18:aaf:6900::a`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeAAAA ns.sslip.io. \? 2600:1f18:aaf:6900::a, 2600:1900:4000:4d12::\n`))
|
|
})
|
|
})
|
|
When("there are multiple MX records returned (e.g. sslip.io)", func() {
|
|
It("returns all the records", func() {
|
|
digArgs = "@localhost sslip.io mx +short -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`10 mail.protonmail.ch.`))
|
|
Eventually(digSession).Should(Say(`20 mailsec.protonmail.ch.\n$`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeMX sslip.io. \? 10 mail.protonmail.ch., 20 mailsec.protonmail.ch.\n`))
|
|
})
|
|
})
|
|
When("there are multiple NS records returned (e.g. almost any NS query)", func() {
|
|
It("returns all the records", func() {
|
|
digArgs = "@localhost example.com ns -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`flags: qr aa rd; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 7`))
|
|
Eventually(digSession).Should(Say(`;; ANSWER SECTION:`))
|
|
Eventually(digSession).Should(Say(`ns-aws.sslip.io.\n`))
|
|
Eventually(digSession).Should(Say(`ns-azure.sslip.io.\n`))
|
|
Eventually(digSession).Should(Say(`ns-gce.sslip.io.\n`))
|
|
Eventually(digSession).Should(Say(`ns-ovh.sslip.io.\n`))
|
|
Eventually(digSession).Should(Say(`;; ADDITIONAL SECTION:`))
|
|
Eventually(digSession).Should(Say(`ns-aws.sslip.io..*52.0.56.137\n`))
|
|
Eventually(digSession).Should(Say(`ns-aws.sslip.io..*2600:1f18:aaf:6900::a\n`))
|
|
Eventually(digSession).Should(Say(`ns-azure.sslip.io..*52.187.42.158\n`))
|
|
Eventually(digSession).Should(Say(`ns-gce.sslip.io..*104.155.144.4\n`))
|
|
Eventually(digSession).Should(Say(`ns-gce.sslip.io..*2600:1900:4000:4d12::\n`))
|
|
Eventually(digSession).Should(Say(`ns-ovh.sslip.io..*51.75.53.19\n`))
|
|
Eventually(digSession).Should(Say(`ns-ovh.sslip.io..*2001:41d0:602:2313::1\n`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeNS example.com. \? ns-aws.sslip.io., ns-azure.sslip.io., ns-gce.sslip.io., ns-ovh.sslip.io.\n`))
|
|
})
|
|
})
|
|
When(`there are multiple TXT records returned (e.g. SPF for sslip.io)`, func() {
|
|
It("returns the custom TXT records", func() {
|
|
digArgs = "@localhost sslip.io txt +short -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`"protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"`))
|
|
Eventually(digSession).Should(Say(`"v=spf1 include:_spf.protonmail.ch mx ~all"`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT sslip.io. \? \["protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"\], \["v=spf1 include:_spf.protonmail.ch mx ~all"\]\n`))
|
|
})
|
|
})
|
|
When(`a record for an "_acme-challenge" domain is queried`, func() {
|
|
When(`it's an NS record`, func() {
|
|
It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() {
|
|
digArgs = "@localhost _acme-challenge.fe80--.sslip.io ns -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1`))
|
|
Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:`))
|
|
Eventually(digSession).Should(Say(`fe80--.sslip.io.`))
|
|
Eventually(digSession).Should(Say(`;; ADDITIONAL SECTION:`))
|
|
Eventually(digSession).Should(Say(`fe80--.sslip.io..*fe80::\n`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeNS _acme-challenge.fe80--.sslip.io. \? nil, NS fe80--.sslip.io.\n`))
|
|
})
|
|
})
|
|
When(`it's a TXT record`, func() {
|
|
It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() {
|
|
digArgs = "@localhost _acme-challenge.127-0-0-1.sslip.io txt -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1,`))
|
|
Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:\n`))
|
|
Eventually(digSession).Should(Say(`^_acme-challenge.127-0-0-1.sslip.io. 604800 IN NS 127-0-0-1.sslip.io.\n`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT _acme-challenge.127-0-0-1.sslip.io. \? nil, NS 127-0-0-1.sslip.io.\n`))
|
|
})
|
|
})
|
|
When(`it's a A record`, func() {
|
|
It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() {
|
|
digArgs = "@localhost _acme-challenge.127-0-0-1.sslip.io a -p " + strconv.Itoa(port)
|
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1,`))
|
|
Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:\n`))
|
|
Eventually(digSession).Should(Say(`^_acme-challenge.127-0-0-1.sslip.io. 604800 IN NS 127-0-0-1.sslip.io.\n`))
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA _acme-challenge.127-0-0-1.sslip.io. \? nil, NS 127-0-0-1.sslip.io.\n`))
|
|
})
|
|
})
|
|
})
|
|
When(`a TXT record for an "metrics.status.sslip.io" domain is repeatedly queried`, 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
|
|
// double the the number of queries to make sure we exhaust the channel's buffers
|
|
for i := 0; i < xip.MetricsBufferSize*2; i++ {
|
|
start = time.Now()
|
|
digArgs = "@localhost metrics.status.sslip.io txt -p " + strconv.Itoa(port)
|
|
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())
|
|
})
|
|
})
|
|
})
|
|
Describe(`The domain blocklist`, func() {
|
|
DescribeTable("when queried",
|
|
func(digArgs string, digResults string, serverLogMessage string) {
|
|
digArgs += " -p " + strconv.Itoa(port)
|
|
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 compare the _full_ contents of the stdout in the case
|
|
// of negative assertions (e.g. "^$")
|
|
Eventually(digSession, 1).Should(Exit(0))
|
|
Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(digResults))
|
|
Eventually(serverSession.Err).Should(Say(serverLogMessage))
|
|
},
|
|
Entry("an A record with a forbidden string on the left-hand side is redirected",
|
|
"@localhost raiffeisen.94.228.116.140.sslip.io +short",
|
|
`\A52.0.56.137\n\z`,
|
|
`TypeA raiffeisen.94.228.116.140.sslip.io. \? 52.0.56.137\n$`),
|
|
Entry("an A record with a forbidden string on the right-hand side is redirected",
|
|
"@localhost www.94-228-116-140.raiffeisen.com +short",
|
|
`\A52.0.56.137\n\z`,
|
|
`TypeA www.94-228-116-140.raiffeisen.com. \? 52.0.56.137\n$`),
|
|
Entry("an A record with a forbidden string embedded is redirected",
|
|
"@localhost international-raiffeisen-bank.94.228.116.140.sslip.io +short",
|
|
`\A52.0.56.137\n\z`,
|
|
`TypeA international-raiffeisen-bank.94.228.116.140.sslip.io. \? 52.0.56.137\n$`),
|
|
Entry("an A record with a forbidden string with a private IP is not redirected",
|
|
"@localhost raiffeisen.192.168.0.20.sslip.io +short",
|
|
`\A192.168.0.20\n\z`,
|
|
`TypeA raiffeisen.192.168.0.20.sslip.io. \? 192.168.0.20\n$`),
|
|
Entry("an AAAA record with a forbidden string is redirected",
|
|
"@localhost international-raiffeisen-bank.2600--.sslip.io aaaa +short",
|
|
`\A2600:1f18:aaf:6900::a\n\z`,
|
|
`TypeAAAA international-raiffeisen-bank.2600--.sslip.io. \? 2600:1f18:aaf:6900::a\n$`),
|
|
Entry("an AAAA record with a forbidden string with a private IP is NOT redirected",
|
|
"@localhost international-raiffeisen-bank.fc00--.sslip.io aaaa +short",
|
|
`\Afc00::\n\z`,
|
|
`TypeAAAA international-raiffeisen-bank.fc00--.sslip.io. \? fc00::\n$`),
|
|
Entry("an NS record with acme_challenge with a forbidden string is not delegated",
|
|
"@localhost _acme-challenge.raiffeisen.fe80--.sslip.io ns +short",
|
|
`\Ans-aws.sslip.io.\nns-azure.sslip.io.\nns-gce.sslip.io.\nns-ovh.sslip.io.\n\z`,
|
|
`TypeNS _acme-challenge.raiffeisen.fe80--.sslip.io. \? ns-aws.sslip.io., ns-azure.sslip.io., ns-gce.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",
|
|
`\A52.0.56.137\n\z`,
|
|
`TypeA nf.43.134.66.67.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",
|
|
`\A2600:1f18:aaf:6900::a\n\z`,
|
|
`TypeAAAA 2601-646-100-69f7-cafe-bebe-cafe-baba.sslip.io. \? 2600:1f18:aaf:6900::a\n$`),
|
|
)
|
|
})
|
|
When("it can't bind to any UDP port", func() {
|
|
It("prints an error message and exits", func() {
|
|
Expect(err).ToNot(HaveOccurred())
|
|
secondServerCmd := exec.Command(serverPath, "-port", strconv.Itoa(port), "-blocklistURL", "file://etc/blocklist-test.txt")
|
|
secondServerSession, err := Start(secondServerCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(secondServerSession.Err, 10).Should(Say("I couldn't bind via UDP to any IPs"))
|
|
Eventually(secondServerSession).Should(Exit(1))
|
|
})
|
|
})
|
|
When("it can't bind to any TCP port", func() {
|
|
var squatters []net.Listener
|
|
var newPort = getFreePort() // I need a new free port to bind on because a server is running on the old port
|
|
BeforeEach(func() {
|
|
squatters, err = squatOnTcp(newPort)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
AfterEach(func() {
|
|
for _, squatter := range squatters {
|
|
err = squatter.Close()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
}
|
|
})
|
|
It("prints an error message and continues running", func() {
|
|
Expect(err).ToNot(HaveOccurred())
|
|
secondServerCmd := exec.Command(serverPath, "-port", strconv.Itoa(newPort), "-blocklistURL", "file://etc/blocklist-test.txt")
|
|
secondServerSession, err := Start(secondServerCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(secondServerSession.Err, 10).Should(Say(` version \d+\.\d+\.\d+ starting`))
|
|
Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via TCP to "\[::\]:\d+" \(INADDR_ANY, all interfaces\), so I'll try to bind to each address individually.`))
|
|
Eventually(secondServerSession.Err, 10).Should(Say("I couldn't bind via TCP to any IPs"))
|
|
Eventually(secondServerSession.Err, 10).Should(Say("Ready to answer queries"))
|
|
secondServerSession.Terminate()
|
|
Eventually(secondServerSession).Should(Exit())
|
|
})
|
|
})
|
|
When("it can't bind via UDP to the loopback address", func() {
|
|
var newPort = getFreePort() // I need a new free port to bind on because the server has already bound to the old port
|
|
var squatter *net.UDPConn
|
|
BeforeEach(func() {
|
|
squatter, err = squatOnUdpLoopbackPort(newPort)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
It("prints an informative message and binds to the addresses it can", func() {
|
|
Expect(err).ToNot(HaveOccurred())
|
|
secondServerCmd := exec.Command(serverPath, "-port", strconv.Itoa(newPort), "-blocklistURL", "file://etc/blocklist-test.txt")
|
|
secondServerSession, err := Start(secondServerCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(secondServerSession.Err, 10).Should(Say(` version \d+\.\d+\.\d+ starting`))
|
|
Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via UDP to "\[::\]:\d+" \(INADDR_ANY, all interfaces\), so I'll try to bind to each address individually.`))
|
|
Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via UDP to the following IPs:.* "(::1|127\.0\.0\.1)"`))
|
|
err = squatter.Close()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(secondServerSession.Err, 10).Should(Say("Ready to answer queries"))
|
|
secondServerSession.Terminate()
|
|
Eventually(secondServerSession).Should(Exit())
|
|
})
|
|
})
|
|
When("it can't bind via TCP to the loopback address", func() {
|
|
var newPort = getFreePort() // I need a new free port to bind on because the server has already bound to the old port
|
|
var squatters []net.Listener
|
|
BeforeEach(func() {
|
|
squatters = squatOnTcpLoopback(newPort)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
It("prints an informative message and binds to the addresses it can", func() {
|
|
Expect(err).ToNot(HaveOccurred())
|
|
secondServerCmd := exec.Command(serverPath, "-port", strconv.Itoa(newPort), "-blocklistURL", "file://etc/blocklist-test.txt")
|
|
secondServerSession, err := Start(secondServerCmd, GinkgoWriter, GinkgoWriter)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Eventually(secondServerSession.Err, 10).Should(Say(` version \d+\.\d+\.\d+ starting`))
|
|
Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via TCP to "\[::\]:\d+" \(INADDR_ANY, all interfaces\), so I'll try to bind to each address individually.`))
|
|
Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via TCP to the following IPs:.* "(::1|127\.0\.0\.1)"`))
|
|
for _, squatter := range squatters {
|
|
err = squatter.Close()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
}
|
|
Eventually(secondServerSession.Err, 10).Should(Say("Ready to answer queries"))
|
|
secondServerSession.Terminate()
|
|
Eventually(secondServerSession).Should(Exit())
|
|
})
|
|
})
|
|
})
|
|
|
|
func squatOnUdpLoopbackPort(port int) (squatter *net.UDPConn, err error) {
|
|
// try IPv6's loopback
|
|
udpAddr := net.UDPAddr{
|
|
IP: net.ParseIP("::1"),
|
|
Port: port,
|
|
}
|
|
squatter, err = net.ListenUDP("udp", &udpAddr)
|
|
if err != nil {
|
|
// try IPv4's loopback
|
|
udpAddr = net.UDPAddr{
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
Port: port,
|
|
}
|
|
squatter, err = net.ListenUDP("udp", &udpAddr)
|
|
}
|
|
return squatter, err
|
|
}
|
|
|
|
func squatOnTcpLoopback(port int) (squatters []net.Listener) {
|
|
var addrsToBind = []string{"[::1]", "127.0.0.1"}
|
|
// Get the GOOS environment variable to determine the operating system.
|
|
goos := strings.ToLower(runtime.GOOS)
|
|
if goos == "darwin" {
|
|
// macOS needs to bind to _all_ interfaces as well; don't know why
|
|
addrsToBind = append([]string{"[::]"}, addrsToBind...)
|
|
}
|
|
// try to bind to both IPv4 and IPv6's loopback
|
|
for _, addr := range addrsToBind {
|
|
addrPort := addr + ":" + strconv.Itoa(port)
|
|
squatter, err := net.Listen("tcp", addrPort)
|
|
if err != nil {
|
|
continue // probably
|
|
}
|
|
squatters = append(squatters, squatter)
|
|
}
|
|
return squatters
|
|
}
|
|
|
|
// squatOnTcp(port) makes any subsequent attempt to bind to that port to fail, for testing purposes
|
|
func squatOnTcp(port int) (squatters []net.Listener, err error) {
|
|
/*
|
|
on macOS, not only do I need to listen on ALL addresses, but I also
|
|
need to listen to addresses individually. This isn't the case with Linux.
|
|
On a typical macOS dual-stack machine, I'll be able to create ~7 Listeners
|
|
(INADDR_ANY, 2 loopback, 1 IPv4, 3 IPv6)
|
|
*/
|
|
var squatter net.Listener
|
|
squatter, err = net.Listen("tcp", ":"+strconv.Itoa(port))
|
|
if err != nil {
|
|
//log.Println(err.Error())
|
|
} else {
|
|
squatters = append(squatters, squatter)
|
|
}
|
|
addrCIDRs, err := net.InterfaceAddrs() // typical addrCIDR "10.9.9.161/24"
|
|
for _, addrCIDR := range addrCIDRs {
|
|
ip, _, err := net.ParseCIDR(addrCIDR.String())
|
|
if err != nil {
|
|
return squatters, err
|
|
}
|
|
ipv6, err := regexp.MatchString(`:`, addrCIDR.String())
|
|
if err != nil {
|
|
return squatters, err
|
|
}
|
|
// accommodate IPv6's requirements for brackets: "[::1]:1024" vs "127.0.0.1:1024"
|
|
if ipv6 {
|
|
squatter, err = net.Listen("tcp", "["+ip.String()+"]"+":"+strconv.Itoa(port))
|
|
//log.Println("[" + ip.String() + "]" + ":" + strconv.Itoa(port))
|
|
if err != nil {
|
|
//log.Println(err.Error())
|
|
// ignore errors on IPv6 bind attempts; it's probably a link-local, which needs a scope
|
|
// https://stackoverflow.com/questions/2455762/why-cant-i-bind-ipv6-socket-to-a-linklocal-address
|
|
continue
|
|
}
|
|
} else {
|
|
squatter, err = net.Listen("tcp", ip.String()+":"+strconv.Itoa(port))
|
|
//log.Println(ip.String() + ":" + strconv.Itoa(port))
|
|
if err != nil {
|
|
//log.Println(err.Error())
|
|
continue
|
|
}
|
|
}
|
|
squatters = append(squatters, squatter)
|
|
}
|
|
//log.Println(len(squatters))
|
|
return squatters, err
|
|
}
|
|
|
|
// getFreePort should always succeed unless something awful has happened, e.g. port exhaustion
|
|
func getFreePort() int {
|
|
// we use a time-based seed to generate a random port to avoid collisions in our test
|
|
// we also bind for a millisecond (in `isPortFree()` to make sure we don't collide
|
|
// with another test running in parallel
|
|
listenPort := (time.Now().Nanosecond() % (65536 - 1024)) + 1023
|
|
for {
|
|
listenPort += 1
|
|
switch {
|
|
case listenPort > 65535:
|
|
listenPort = 1023 // we've reached the highest port, start over
|
|
// 1024 (lowest unprivileged port) - 1 (immediately incremented)
|
|
case isPortFree(listenPort):
|
|
return listenPort
|
|
}
|
|
}
|
|
}
|
|
|
|
func isPortFree(port int) bool {
|
|
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// we must Sleep() in order to avoid a race condition when tests
|
|
// are run in parallel (`ginkgo -p`) and the `ListenUDP()` and `Close()`
|
|
// we sleep for a millisecond because the port is randomized based on the millisecond.
|
|
time.Sleep(1 * time.Millisecond)
|
|
err = conn.Close()
|
|
if err != nil {
|
|
log.Printf("I couldn't close port %d", port)
|
|
return false
|
|
}
|
|
return true
|
|
}
|