From 78722b68874e873521b1257a6f082e59e61b8fa5 Mon Sep 17 00:00:00 2001
From: Brian Cunnie
Date: Tue, 30 Nov 2021 05:39:57 -0800
Subject: [PATCH] `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:
```shell
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:
```shell
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:
```shell
dig @ns-gce.sslip.io. txt macos-version.kv.sslip.io.
```
Finally, when you're done with the key-value, you can "delete" it:
```shell
dig @ns-gce.sslip.io. txt delete.macos-version.kv.sslip.io.
```
Notes:
- Keys are case-insensitive (to accommodate DNS convention). In other
words, `KEY.kv.sslip.io` and `key.kv.sslip.io` return the same TXT
record.
- Values are case-sensitive. `put.CamelCase.style.kv.sslip.io` sets the
TXT record to "CamelCase".
- `put` requests will return the TXT record being put; i.e.
`put.hello.world.kv.sslip.io` returns one TXT record of one string,
`hello`.
- `delete` requests will return the TXT record being deleted; i.e.
`delete.world.kv.sslip.io` returns one TXT record of one string,
`hello`. If the TXT record does not exist, no TXT records will be
returned.
- Values are limited to 63 bytes to mitigate using the sslip.io servers
in a [DNS amplification
attack](https://us-cert.cisa.gov/ncas/alerts/TA13-088A).
- Values are not persistent: if the server is restarted, all values
disappear. Poof.
- Values are not consistent. If a value is set in `ns-aws.sslip.io`, it
does not propagate to `ns-gce.sslip.io` nor `ns-azure.sslip.io`.
---
.../sslip.io-dns-server/integration_test.go | 20 ++++++
.../src/sslip.io-dns-server/xip/xip.go | 65 ++++++++++++++++++-
.../src/sslip.io-dns-server/xip/xip_test.go | 33 ++++++++++
k8s/document_root/index.html | 46 +++++++++++++
4 files changed, 163 insertions(+), 1 deletion(-)
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:
+
+ - Keys are case-insensitive (to accommodate DNS convention). In other words,
KEY.kv.sslip.io
and
+ key.kv.sslip.io
return the same TXT record.
+ - Values are case-sensitive.
put.CamelCase.style.kv.sslip.io
sets the TXT record to
+ "CamelCase".
+ put
requests will return the TXT record being put; i.e.
+ put.hello.world.kv.sslip.io
returns one TXT record of one string, hello
.
+ delete
requests will return the TXT record being deleted; i.e.
+ delete.world.kv.sslip.io
returns one TXT record of one string, hello
. If the TXT
+ record does not exist, no TXT records will be returned.
+ - Values are limited to 63 bytes to mitigate using the sslip.io servers in a DNS amplification attack.
+
+ - Values are not persistent: if the server is restarted, all values disappear. Poof.
+ - Values are not consistent. If a value is set in
ns-aws.sslip.io
, it does not propagate to
+ ns-gce.sslip.io
nor ns-azure.sslip.io
.
+
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
: