Key-value store domain: kv.sslip.io → k-v.io

I didn't want a really long domain for the key-value store; I wanted a
short, easy-to-remember domain. And it cost $400 for ten years.

Many good domains (e.g. keyvalue.store, kv.io)
were taken, and some weren't easily registered (e.g. the Albanian
domain, keyv.al).

Browsing these domains that were never put into use is like strolling
along the Boulevard of Broken Dreams: high hopes dashed against the hard
rocks of reality.
This commit is contained in:
Brian Cunnie
2021-12-29 19:56:52 -08:00
parent 3066a22f57
commit 5065229a03
4 changed files with 68 additions and 81 deletions

View File

@@ -121,26 +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",
Entry(`getting a non-existent value: TXT for my-key.k-v.io"`,
"@127.0.0.1 my-key.k-v.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",
`TypeTXT my-key.k-v.io. \? nil, SOA my-key.k-v.io. briancunnie.gmail.com. 2021080200 900 900 1800 300\n$`),
Entry(`putting a value: TXT for put.MyValue.MY-KEY.k-v.io"`,
"@127.0.0.1 put.MyValue.MY-KEY.k-v.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",
`TypeTXT put.MyValue.MY-KEY.k-v.io. \? \["MyValue"\]`),
Entry(`getting a value: TXT for my-key.k-v.io"`,
"@127.0.0.1 my-key.k-v.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",
`TypeTXT my-key.k-v.io. \? \["MyValue"\]`),
Entry(`deleting a value: TXT for delete.my-key.k-v.io"`,
"@127.0.0.1 delete.my-key.k-v.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",
`TypeTXT delete.my-key.k-v.io. \? \["MyValue"\]`),
Entry(`getting a non-existent value: TXT for my-key.k-v.io"`,
"@127.0.0.1 my-key.k-v.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$`),
`TypeTXT my-key.k-v.io. \? nil, SOA my-key.k-v.io. briancunnie.gmail.com. 2021080200 900 900 1800 300\n$`),
)
})
Describe("for more complex assertions", func() {
@@ -225,33 +225,33 @@ var _ = Describe("sslip.io-dns-server", func() {
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT sslip.io. \? \["protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"\], \["v=spf1 include:_spf.protonmail.ch mx ~all"\]\n`))
})
})
When(`a TXT record for a host under the "kv.sslip.io" domain is queried`, func() {
When(`a TXT record for a host under the "k-v.io" domain is queried`, func() {
It(`the PUT has a three-minute TTL`, func() {
digArgs = "@localhost put.a.b.kv.sslip.io txt"
digArgs = "@localhost put.a.b.k-v.io txt"
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())
Eventually(digSession).Should(Say(`put.a.b.kv.sslip.io. 180 IN TXT "a"`))
Eventually(digSession).Should(Say(`put.a.b.k-v.io. 180 IN TXT "a"`))
Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT put.a.b.kv.sslip.io. \? \["a"\]`))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT put.a.b.k-v.io. \? \["a"\]`))
})
It(`the GET has a three-minute TTL`, func() {
digArgs = "@localhost b.kv.sslip.io txt"
digArgs = "@localhost b.k-v.io txt"
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())
Eventually(digSession).Should(Say(`b.kv.sslip.io. 180 IN TXT "a"`))
Eventually(digSession).Should(Say(`b.k-v.io. 180 IN TXT "a"`))
Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT b.kv.sslip.io. \? \["a"\]`))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT b.k-v.io. \? \["a"\]`))
})
It(`the DELETE has a three-minute TTL`, func() {
digArgs = "@localhost delete.b.kv.sslip.io txt"
digArgs = "@localhost delete.b.k-v.io txt"
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())
Eventually(digSession).Should(Say(`delete.b.kv.sslip.io. 180 IN TXT "a"`))
Eventually(digSession).Should(Say(`delete.b.k-v.io. 180 IN TXT "a"`))
Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT delete.b.kv.sslip.io. \? \["a"\]`))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT delete.b.k-v.io. \? \["a"\]`))
})
})
When(`a record for an "_acme-challenge" domain is queried`, func() {

View File

@@ -20,7 +20,7 @@ import (
// Xip contains info that the routines need to answer a query that I don't want to plumb
// through the call hierarchy
// (the source address for `ip.sslip.io`, and the etcd client for `kv.sslip.io`)
// (the source address for `ip.sslip.io`, and the etcd client for `k-v.io`)
type Xip struct {
SrcAddr net.IP
Etcd *v3client.Client
@@ -53,7 +53,7 @@ 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."
// The key should NOT include ".k-v.io."
type KvCustomizations map[string][]dnsmessage.TXTResource
// There's nothing like global variables to make my heart pound with joy.
@@ -66,7 +66,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\.$`)
kvRE = regexp.MustCompile(`\.k-v\.io\.$`)
nsAwsSslip, _ = dnsmessage.NewName("ns-aws.sslip.io.")
nsAzureSslip, _ = dnsmessage.NewName("ns-azure.sslip.io.")
nsGceSslip, _ = dnsmessage.NewName("ns-gce.sslip.io.")
@@ -745,27 +745,27 @@ 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
// when TXT for "k-v.io" is queried, return the key-value pair
func (x Xip) kvTXTResources(fqdn string) ([]dnsmessage.TXTResource, error) {
// "labels" => official RFC 1035 term
// kv.sslip.io. => ["kv", "sslip", "io"] are labels
// k-v.io. => ["k-v", "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"
key string // e.g. "my-key" as in "my-key.k-v.io"
value string // e.g. "my-value" as in "put.my-value.my-key.k-v.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"
labels = labels[:len(labels)-3] // strip ".k-v.io"
key = strings.ToLower(labels[len(labels)-1]) // key is always present, always first subdomain of "k-v.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"
verb = strings.ToLower(labels[0]) // verb, if present, is leftmost, "put.value.key.k-v.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"
value = strings.Join(labels[1:len(labels)-1], ".") // e.g. "put.94.0.2.firefox-version.k-v.io"
}
// prepare to query etcd:
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)

View File

@@ -172,7 +172,7 @@ var _ = Describe("Xip", func() {
AfterEach(func() {
x.Etcd.Close()
})
DescribeTable(`the domain "kv.sslip.io" is queried`,
DescribeTable(`the domain "k-v.io" is queried`,
func(fqdn string, txts []string) {
txtResources, err := x.TXTResources(fqdn)
Expect(err).ToNot(HaveOccurred())
@@ -183,25 +183,25 @@ var _ = Describe("Xip", func() {
}
},
// 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{}),
Entry("no arguments → empty array", "k-v.io.", []string{}),
Entry("putting a value → that value", "PUT.MyValue.my-key.k-v.io.", []string{"MyValue"}),
Entry("getting that value → that value", "my-key.k-v.io.", []string{"MyValue"}),
Entry("getting that value with an UPPERCASE key → that value", "MY-KEY.k-v.io.", []string{"MyValue"}),
Entry("explicitly getting that value → that value", "GeT.my-key.k-v.io.", []string{"MyValue"}),
Entry("deleting that value → the deleted value", "DelETe.my-key.k-v.io.", []string{"MyValue"}),
Entry("getting that deleted value → empty array", "my-key.k-v.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"}),
Entry("getting a non-existent key → empty array", "nonexistent.k-v.io.", []string{}),
Entry("putting but skipping the value → error txt", "put.my-key.k-v.io.", []string{"422: no value provided"}),
Entry("deleting a non-existent key → silently succeeds", "delete.non-existent.k-v.io.", []string{}),
Entry("using a garbage verb → error txt", "post.my-key.k-v.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 multi-label value", "put.96.0.4664.55.chrome-version.k-v.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.",
".amplify.k-v.io.",
[]string{"IReturnedAndSawUnderTheSunThatTheRaceIsNotToTheSwiftNotThe.Batt"},
),
)

View File

@@ -227,45 +227,32 @@ 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>
<h4 id="key-value-store"><code>k-v.io</code>: (key-value) read/write/delete TXTs</h4>
<p>We enable special behavior under the <code>k-v.io</code> domain: it can be treated as a key-value store, the
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>
<pre><code class="lang-shell">dig @ns-gce.sslip.io. txt put.12.0.1.macos-version.k-v.io</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>
<pre><code class="lang-shell">dig @ns-gce.sslip.io. txt get.macos-version.k-v.io.</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>
<pre><code class="lang-shell">dig @ns-gce.sslip.io. txt macos-version.k-v.io.</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>.
<pre><code class="lang-shell">dig @ns-gce.sslip.io. txt delete.macos-version.k-v.io.
</code></pre>
<p>Notes:</p>
<p><code class="lang-shell">Notes:</code></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>Keys are case-insensitive (to accommodate DNS convention). In other words,
<code>KEY.k-v.io</code> and <code>key.k-v.io</code> return the same TXT record.</code></li>
<li>Values are case-sensitive. <code>put.CamelCase.style.k-v.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.k-v.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>
<code>delete.world.k-v.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>