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 a8f515b..683c338 100644 --- a/bosh-release/src/sslip.io-dns-server/integration_test.go +++ b/bosh-release/src/sslip.io-dns-server/integration_test.go @@ -121,6 +121,26 @@ var _ = Describe("sslip.io-dns-server", func() { "@127.0.0.1 example.com txt +short", `\A\z`, `TypeTXT example.com. \? nil, SOA example.com. briancunnie.gmail.com. 2021080200 900 900 1800 300\n$`), + Entry(`getting a non-existent value: TXT for my-key.kv.sslip.io"`, + "@127.0.0.1 my-key.kv.sslip.io txt +short", + `\A\z`, + `TypeTXT my-key.kv.sslip.io. \? nil, SOA my-key.kv.sslip.io. briancunnie.gmail.com. 2021080200 900 900 1800 300\n$`), + Entry(`putting a value: TXT for put.MyValue.MY-KEY.kv.sslip.io"`, + "@127.0.0.1 put.MyValue.MY-KEY.kv.sslip.io txt +short", + `"MyValue"`, + `TypeTXT put.MyValue.MY-KEY.kv.sslip.io. \? \["MyValue"\]`), + Entry(`getting a value: TXT for my-key.kv.sslip.io"`, + "@127.0.0.1 my-key.kv.sslip.io txt +short", + `"MyValue"`, + `TypeTXT my-key.kv.sslip.io. \? \["MyValue"\]`), + Entry(`deleting a value: TXT for delete.my-key.kv.sslip.io"`, + "@127.0.0.1 delete.my-key.kv.sslip.io txt +short", + `"MyValue"`, + `TypeTXT delete.my-key.kv.sslip.io. \? \["MyValue"\]`), + Entry(`getting a non-existent value: TXT for my-key.kv.sslip.io"`, + "@127.0.0.1 my-key.kv.sslip.io txt +short", + `\A\z`, + `TypeTXT my-key.kv.sslip.io. \? nil, SOA my-key.kv.sslip.io. briancunnie.gmail.com. 2021080200 900 900 1800 300\n$`), ) }) Describe("for more complex assertions", func() { 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 766dc2f..4b4ff4d 100644 --- a/bosh-release/src/sslip.io-dns-server/xip/xip.go +++ b/bosh-release/src/sslip.io-dns-server/xip/xip.go @@ -31,11 +31,18 @@ type DomainCustomization struct { // e.g. IP address of the query's source } +// DomainCustomizations is a lookup table for specially-crafted records +// e.g. MX records for sslip.io. // The string key should always be lower-cased // DomainCustomizations{"sslip.io": ...} NOT DomainCustomizations{"sSLip.iO": ...} // DNS hostnames are technically case-insensitive type DomainCustomizations map[string]DomainCustomization +// KvCustomizations is a lookup table for custom TXT records +// e.g. KvCustomizations["my-key"] = []dnsmessage.TXTResource{ TXT: { "my-value" } } +// The key should NOT include ".kv.sslip.io." +type KvCustomizations map[string][]dnsmessage.TXTResource + // There's nothing like global variables to make my heart pound with joy. // Some of these are global because they are, in essence, constants which // I don't want to waste time recreating with every function call. @@ -46,6 +53,7 @@ var ( // https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses ipv6RE = regexp.MustCompile(`(^|[.-])(([0-9a-fA-F]{1,4}-){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}-){1,7}-|([0-9a-fA-F]{1,4}-){1,6}-[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}-){1,5}(-[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}-){1,4}(-[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}-){1,3}(-[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}-){1,2}(-[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}-((-[0-9a-fA-F]{1,4}){1,6})|-((-[0-9a-fA-F]{1,4}){1,7}|-)|fe80-(-[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|--(ffff(-0{1,4})?-)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}-){1,4}-((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))($|[.-])`) dns01ChallengeRE = regexp.MustCompile(`(?i)_acme-challenge\.`) + kvRE = regexp.MustCompile(`\.kv\.sslip\.io\.$`) nsAwsSslip, _ = dnsmessage.NewName("ns-aws.sslip.io.") nsAzureSslip, _ = dnsmessage.NewName("ns-azure.sslip.io.") nsGceSslip, _ = dnsmessage.NewName("ns-gce.sslip.io.") @@ -66,7 +74,8 @@ var ( VersionDate = "today" VersionGitHash = "xxx" - Customizations = DomainCustomizations{ + TxtKvCustomizations = KvCustomizations{} + Customizations = DomainCustomizations{ "sslip.io.": { A: []dnsmessage.AResource{ {A: [4]byte{78, 46, 204, 247}}, @@ -691,6 +700,9 @@ func NSResources(fqdnString string) []dnsmessage.NSResource { // TXTResources returns TXT records from Customizations func TXTResources(fqdn, querier string) ([]dnsmessage.TXTResource, error) { + if kvRE.MatchString(fqdn) { + return kvTXTResources(fqdn) + } if domain, ok := Customizations[strings.ToLower(fqdn)]; ok { return domain.TXT(querier) } @@ -726,6 +738,57 @@ func ipSslipIo(sourceIP string) ([]dnsmessage.TXTResource, error) { return []dnsmessage.TXTResource{{TXT: []string{sourceIP}}}, nil } +// when TXT for "kv.sslip.io" is queried, return the key-value pair +func kvTXTResources(fqdn string) ([]dnsmessage.TXTResource, error) { + // "labels" => official RFC 1035 term + // kv.sslip.io. => ["kv", "sslip", "io"] are labels + var ( + verb string // i.e. "get", "put", "delete" + key string // e.g. "my-key" as in "my-key.kv.sslip.io" + value string // e.g. "my-value" as in "put.my-value.my-key.kv.sslip.io" + ) + labels := strings.Split(fqdn, ".") + labels = labels[:len(labels)-4] // strip ".kv.sslip.io" + key = strings.ToLower(labels[len(labels)-1]) // key is always present, always first subdomain of "kv.sslip.io" + switch { + case len(labels) == 1: + verb = "get" // default action if only key, not verb, is not present + case len(labels) == 2: + verb = strings.ToLower(labels[0]) // verb, if present, is leftmost, "put.value.key.kv.sslip.io" + case len(labels) > 2: + verb = strings.ToLower(labels[0]) + // concatenate multiple labels to create value, especially useful for version numbers + value = strings.Join(labels[1:len(labels)-1], ".") // e.g. "put.94.0.2.firefox-version.kv.sslip.io" + } + switch verb { + case "get": + if txtRecord, ok := TxtKvCustomizations[key]; ok { + return txtRecord, nil + } + return nil, nil + case "put": + if len(labels) == 2 { + return []dnsmessage.TXTResource{{[]string{"422: no value provided"}}}, nil + } + if len(value) > 63 { // too-long TXT records can be used in DNS amplification attacks; Truncate! + value = value[:63] + } + TxtKvCustomizations[key] = []dnsmessage.TXTResource{ + { + []string{value}, + }, + } + return TxtKvCustomizations[key], nil + case "delete": + if deletedKey, ok := TxtKvCustomizations[key]; ok { + delete(TxtKvCustomizations, key) + return deletedKey, nil + } + return nil, nil + } + return []dnsmessage.TXTResource{{[]string{"422: valid verbs are get, put, delete"}}}, nil +} + // soaLogMessage returns an easy-to-read string for logging SOA Answers/Authorities func soaLogMessage(soaResource dnsmessage.SOAResource) string { return soaResource.NS.String() + " " + diff --git a/bosh-release/src/sslip.io-dns-server/xip/xip_test.go b/bosh-release/src/sslip.io-dns-server/xip/xip_test.go index b633912..e69c670 100644 --- a/bosh-release/src/sslip.io-dns-server/xip/xip_test.go +++ b/bosh-release/src/sslip.io-dns-server/xip/xip_test.go @@ -183,6 +183,39 @@ var _ = Describe("Xip", func() { Expect(txts[0].TXT[0]).To(MatchRegexp("^1.1.1.1$")) }) }) + DescribeTable(`the domain "kv.sslip.io" is queried`, + func(fqdn string, txts []string) { + txtResources, err := xip.TXTResources(fqdn, "querier's IP address doesn't matter") + Expect(err).ToNot(HaveOccurred()) + Expect(len(txtResources)).To(Equal(len(txts))) + for i, txtResource := range txtResources { + Expect(len(txtResource.TXT)).To(Equal(1)) // each TXT record has 1 & only 1 string + Expect(txtResource.TXT[0]).To(Equal(txts[i])) + } + }, + // simple tests: get, put, delete with single label value + Entry("no arguments → empty array", "kv.sslip.io.", []string{}), + Entry("putting a value → that value", "PUT.MyValue.my-key.kv.sslip.io.", []string{"MyValue"}), + Entry("getting that value → that value", "my-key.kv.sslip.io.", []string{"MyValue"}), + Entry("getting that value with an UPPERCASE key → that value", "MY-KEY.kv.sslip.io.", []string{"MyValue"}), + Entry("explicitly getting that value → that value", "GeT.my-key.kv.sslip.io.", []string{"MyValue"}), + Entry("deleting that value → the deleted value", "DelETe.my-key.kv.sslip.io.", []string{"MyValue"}), + Entry("getting that deleted value → empty array", "my-key.kv.sslip.io.", []string{}), + // errors + Entry("getting a non-existent key → empty array", "nonexistent.kv.sslip.io.", []string{}), + Entry("putting but skipping the value → error txt", "put.my-key.kv.sslip.io.", []string{"422: no value provided"}), + Entry("deleting a non-existent key → silently succeeds", "delete.non-existent.kv.sslip.io.", []string{}), + Entry("using a garbage verb → error txt", "post.my-key.kv.sslip.io.", []string{"422: valid verbs are get, put, delete"}), + // others + Entry("putting a multi-label value", "put.96.0.4664.55.chrome-version.kv.sslip.io.", []string{"96.0.4664.55"}), + Entry("putting a super-long multi-label value to use in a DNS amplification attack gets truncated to 63 characters", + "put"+ + ".IReturnedAndSawUnderTheSunThatTheRaceIsNotToTheSwiftNotThe"+ + ".BattleToTheStrongNeitherYetBreadToTheWiseNorYetRichesToMenOf"+ + ".amplify.kv.sslip.io.", + []string{"IReturnedAndSawUnderTheSunThatTheRaceIsNotToTheSwiftNotThe.Batt"}, + ), + ) }) Describe("NameToA()", func() { diff --git a/k8s/document_root/index.html b/k8s/document_root/index.html index 3be046b..db026f9 100644 --- a/k8s/document_root/index.html +++ b/k8s/document_root/index.html @@ -227,6 +227,52 @@ dig @ns.sslip.io txt ip.sslip.io +short -6 # forces IPv6 lookup; sample reply "2 "https://icanhazip.com/">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.

+

kv.sslip.io: (key-value) read/write/delete TXTs

+

We enable special behavior under the kv.sslip.io subdomain: it can be treated as a key-value + store, the sub-subdomain being the key, and the TXT record being the value.

+

For example, to write ("put") the value "12.0.1" to the key "macos-version" on the + ns-gce.sslip.io. nameserver, you'd use the following dig command:

+
dig @ns-gce.sslip.io. txt put.12.0.1.macos-version.kv.sslip.io.
+
+

To read ("get") the value back, you'd write the following dig command:

+
dig @ns-gce.sslip.io. txt get.macos-version.kv.sslip.io.
+
+

Since "get" is the default behavior, you don't need to include it in the domain name:

+
dig @ns-gce.sslip.io. txt macos-version.kv.sslip.io.
+
+

Finally, when you're done with the key-value, you can "delete" it:

+
dig @ns-gce.sslip.io. txt delete.macos-version.kv.sslip.io.
+
+

Notes:

+

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: