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`.
This commit is contained in:
Brian Cunnie
2021-11-30 05:39:57 -08:00
parent 4ba3516834
commit 78722b6887
4 changed files with 163 additions and 1 deletions

View File

@@ -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() {

View File

@@ -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,6 +74,7 @@ var (
VersionDate = "today"
VersionGitHash = "xxx"
TxtKvCustomizations = KvCustomizations{}
Customizations = DomainCustomizations{
"sslip.io.": {
A: []dnsmessage.AResource{
@@ -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() + " " +

View File

@@ -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() {

View File

@@ -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/</a> 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.</p>
<h4 id="key-value-store"><code>kv.sslip.io</code>: (key-value) read/write/delete TXTs</h4>
<p>We enable special behavior under the <code>kv.sslip.io</code> subdomain: it can be treated as a key-value
store, the sub-subdomain being the key, and the TXT record being the value.</p>
<p>For example, to write ("put") the value "12.0.1" to the key "macos-version" on the
<code>ns-gce.sslip.io.</code> nameserver, you'd use the following <code>dig</code> command:</p>
<pre><code class="lang-shell">dig @ns-gce<span class="hljs-selector-class">.sslip</span><span class=
"hljs-selector-class">.io</span>. txt put.<span class="hljs-number">12.0</span>.<span class=
"hljs-number">1</span><span class="hljs-selector-class">.macos-version</span><span class=
"hljs-selector-class">.kv</span><span class="hljs-selector-class">.sslip</span><span class=
"hljs-selector-class">.io</span>.
</code></pre>
<p>To read ("get") the value back, you'd write the following <code>dig</code> command:</p>
<pre><code class="lang-shell">dig @ns-gce<span class="hljs-selector-class">.sslip</span><span class=
"hljs-selector-class">.io</span>. txt get<span class="hljs-selector-class">.macos-version</span><span class=
"hljs-selector-class">.kv</span><span class="hljs-selector-class">.sslip</span><span class=
"hljs-selector-class">.io</span>.
</code></pre>
<p>Since "get" is the default behavior, you don't need to include it in the domain name:</p>
<pre><code class="lang-shell">dig @ns-gce<span class="hljs-selector-class">.sslip</span><span class=
"hljs-selector-class">.io</span>. txt macos-version<span class="hljs-selector-class">.kv</span><span class=
"hljs-selector-class">.sslip</span><span class="hljs-selector-class">.io</span>.
</code></pre>
<p>Finally, when you're done with the key-value, you can "delete" it:</p>
<pre><code class="lang-shell">dig @ns-gce<span class="hljs-selector-class">.sslip</span><span class=
"hljs-selector-class">.io</span>. txt delete<span class="hljs-selector-class">.macos-version</span><span class=
"hljs-selector-class">.kv</span><span class="hljs-selector-class">.sslip</span><span class=
"hljs-selector-class">.io</span>.
</code></pre>
<p>Notes:</p>
<ul>
<li>Keys are case-insensitive (to accommodate DNS convention). In other words, <code>KEY.kv.sslip.io</code> and
<code>key.kv.sslip.io</code> return the same TXT record.</li>
<li>Values are case-sensitive. <code>put.CamelCase.style.kv.sslip.io</code> sets the TXT record to
"CamelCase".</li>
<li><code>put</code> requests will return the TXT record being put; i.e.
<code>put.hello.world.kv.sslip.io</code> returns one TXT record of one string, <code>hello</code>.</li>
<li><code>delete</code> requests will return the TXT record being deleted; i.e.
<code>delete.world.kv.sslip.io</code> returns one TXT record of one string, <code>hello</code>. If the TXT
record does not exist, no TXT records will be returned.</li>
<li>Values are limited to 63 bytes to mitigate using the sslip.io servers in a <a href=
"https://us-cert.cisa.gov/ncas/alerts/TA13-088A">DNS amplification attack</a>.
</li>
<li>Values are not persistent: if the server is restarted, all values disappear. Poof.</li>
<li>Values are not consistent. If a value is set in <code>ns-aws.sslip.io</code>, it does not propagate to
<code>ns-gce.sslip.io</code> nor <code>ns-azure.sslip.io</code>.</li>
</ul>
<h4 id="version">Determining The Server Version of Software</h4>You can determine the server version of the
sslip.io software by querying the TXT record of <code>version.sslip.io</code>:
<pre>