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",
|
"@127.0.0.1 example.com txt +short",
|
||||||
`\A\z`,
|
`\A\z`,
|
||||||
`TypeTXT example.com. \? nil, SOA example.com. briancunnie.gmail.com. 2021080200 900 900 1800 300\n$`),
|
`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() {
|
Describe("for more complex assertions", func() {
|
||||||
|
@@ -31,11 +31,18 @@ type DomainCustomization struct {
|
|||||||
// e.g. IP address of the query's source
|
// 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
|
// The string key should always be lower-cased
|
||||||
// DomainCustomizations{"sslip.io": ...} NOT DomainCustomizations{"sSLip.iO": ...}
|
// DomainCustomizations{"sslip.io": ...} NOT DomainCustomizations{"sSLip.iO": ...}
|
||||||
// DNS hostnames are technically case-insensitive
|
// DNS hostnames are technically case-insensitive
|
||||||
type DomainCustomizations map[string]DomainCustomization
|
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.
|
// 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
|
// Some of these are global because they are, in essence, constants which
|
||||||
// I don't want to waste time recreating with every function call.
|
// 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
|
// 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]))($|[.-])`)
|
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\.`)
|
dns01ChallengeRE = regexp.MustCompile(`(?i)_acme-challenge\.`)
|
||||||
|
kvRE = regexp.MustCompile(`\.kv\.sslip\.io\.$`)
|
||||||
nsAwsSslip, _ = dnsmessage.NewName("ns-aws.sslip.io.")
|
nsAwsSslip, _ = dnsmessage.NewName("ns-aws.sslip.io.")
|
||||||
nsAzureSslip, _ = dnsmessage.NewName("ns-azure.sslip.io.")
|
nsAzureSslip, _ = dnsmessage.NewName("ns-azure.sslip.io.")
|
||||||
nsGceSslip, _ = dnsmessage.NewName("ns-gce.sslip.io.")
|
nsGceSslip, _ = dnsmessage.NewName("ns-gce.sslip.io.")
|
||||||
@@ -66,7 +74,8 @@ var (
|
|||||||
VersionDate = "today"
|
VersionDate = "today"
|
||||||
VersionGitHash = "xxx"
|
VersionGitHash = "xxx"
|
||||||
|
|
||||||
Customizations = DomainCustomizations{
|
TxtKvCustomizations = KvCustomizations{}
|
||||||
|
Customizations = DomainCustomizations{
|
||||||
"sslip.io.": {
|
"sslip.io.": {
|
||||||
A: []dnsmessage.AResource{
|
A: []dnsmessage.AResource{
|
||||||
{A: [4]byte{78, 46, 204, 247}},
|
{A: [4]byte{78, 46, 204, 247}},
|
||||||
@@ -691,6 +700,9 @@ func NSResources(fqdnString string) []dnsmessage.NSResource {
|
|||||||
|
|
||||||
// TXTResources returns TXT records from Customizations
|
// TXTResources returns TXT records from Customizations
|
||||||
func TXTResources(fqdn, querier string) ([]dnsmessage.TXTResource, error) {
|
func TXTResources(fqdn, querier string) ([]dnsmessage.TXTResource, error) {
|
||||||
|
if kvRE.MatchString(fqdn) {
|
||||||
|
return kvTXTResources(fqdn)
|
||||||
|
}
|
||||||
if domain, ok := Customizations[strings.ToLower(fqdn)]; ok {
|
if domain, ok := Customizations[strings.ToLower(fqdn)]; ok {
|
||||||
return domain.TXT(querier)
|
return domain.TXT(querier)
|
||||||
}
|
}
|
||||||
@@ -726,6 +738,57 @@ func ipSslipIo(sourceIP string) ([]dnsmessage.TXTResource, error) {
|
|||||||
return []dnsmessage.TXTResource{{TXT: []string{sourceIP}}}, nil
|
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
|
// soaLogMessage returns an easy-to-read string for logging SOA Answers/Authorities
|
||||||
func soaLogMessage(soaResource dnsmessage.SOAResource) string {
|
func soaLogMessage(soaResource dnsmessage.SOAResource) string {
|
||||||
return soaResource.NS.String() + " " +
|
return soaResource.NS.String() + " " +
|
||||||
|
@@ -183,6 +183,39 @@ var _ = Describe("Xip", func() {
|
|||||||
Expect(txts[0].TXT[0]).To(MatchRegexp("^1.1.1.1$"))
|
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() {
|
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
|
"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
|
as much! Admittedly bandwidth usage is a bigger concern for the one hosting the service than the one using the
|
||||||
service.</p>
|
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
|
<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>:
|
sslip.io software by querying the TXT record of <code>version.sslip.io</code>:
|
||||||
<pre>
|
<pre>
|
||||||
|
Reference in New Issue
Block a user