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