mirror of
https://github.com/cunnie/sslip.io.git
synced 2025-10-05 23:56:50 +08:00
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:
@@ -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() {
|
||||
|
@@ -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)
|
||||
|
@@ -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"},
|
||||
),
|
||||
)
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user