mirror of
https://github.com/cunnie/sslip.io.git
synced 2025-10-07 16:41:19 +08:00
k-v.io: on DELETE, don't return the deleted value
We don't return the deleted value because doing that would have the unintended consequence of postponing the deletion: downstream caching servers would cache the deleted value for up to three more minutes. We'd rather have the key deleted sooner rather than later. Some APIs, e.g. etcd's, return a list of deleted values on return: those APIs can afford to do so because they don't need to worry about DNS propagation. We also lengthen the timeout of an `etcd` API call from 500 msec to 1928 msecs; 500 msec was too close; some calls routinely took 480 msec to complete, and we wanted more headroom. We also no longer do two `etcd` operations when we delete a value. Previously we would do a GET followed by a DELETE, but since we're not returning the value deleted, there's no point to the GET. Furthermore, the GET was never necessary, for the `etcd` DELETE API call returned the values deleted. Drive-by: - README: install gingko the proper way, with `go install` [fixes #17]
This commit is contained in:
@@ -31,7 +31,7 @@ dig @localhost 192.168.0.1.sslip.io +short
|
|||||||
## Quick Start Tests
|
## Quick Start Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get github.com/onsi/ginkgo/v2/ginkgo
|
go install github.com/onsi/ginkgo/v2/ginkgo@latest
|
||||||
go get github.com/onsi/gomega/...
|
go get github.com/onsi/gomega/...
|
||||||
sudo ~/go/bin/ginkgo -r .
|
sudo ~/go/bin/ginkgo -r .
|
||||||
# sudo is required on Linux, but not on macOS, to bind to privileged port 53
|
# sudo is required on Linux, but not on macOS, to bind to privileged port 53
|
||||||
|
@@ -147,8 +147,8 @@ var _ = Describe("sslip.io-dns-server", func() {
|
|||||||
`TypeTXT my-key.k-v.io. \? \["MyValue"\]`),
|
`TypeTXT my-key.k-v.io. \? \["MyValue"\]`),
|
||||||
Entry(`deleting a value: TXT for delete.my-key.k-v.io"`,
|
Entry(`deleting a value: TXT for delete.my-key.k-v.io"`,
|
||||||
"@127.0.0.1 delete.my-key.k-v.io txt +short",
|
"@127.0.0.1 delete.my-key.k-v.io txt +short",
|
||||||
`"MyValue"`,
|
`\A\z`,
|
||||||
`TypeTXT delete.my-key.k-v.io. \? \["MyValue"\]`),
|
`TypeTXT delete.my-key.k-v.io. \? nil, SOA delete.my-key.k-v.io. briancunnie.gmail.com. 2022020800 900 900 1800 180\n$`),
|
||||||
Entry(`getting a non-existent value: TXT for my-key.k-v.io"`,
|
Entry(`getting a non-existent value: TXT for my-key.k-v.io"`,
|
||||||
"@127.0.0.1 my-key.k-v.io txt +short",
|
"@127.0.0.1 my-key.k-v.io txt +short",
|
||||||
`\A\z`,
|
`\A\z`,
|
||||||
@@ -256,14 +256,21 @@ var _ = Describe("sslip.io-dns-server", func() {
|
|||||||
Eventually(digSession, 1).Should(Exit(0))
|
Eventually(digSession, 1).Should(Exit(0))
|
||||||
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT b.k-v.io. \? \["a"\]`))
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT b.k-v.io. \? \["a"\]`))
|
||||||
})
|
})
|
||||||
It(`the DELETE has a three-minute TTL`, func() {
|
It(`the DELETE returns no records so that value cached in downstream DNS servers expires more quickly`, func() {
|
||||||
digArgs = "@localhost delete.b.k-v.io txt -p " + strconv.Itoa(port)
|
digArgs = "@localhost delete.b.k-v.io txt -p " + strconv.Itoa(port)
|
||||||
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
||||||
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Eventually(digSession).Should(Say(`delete.b.k-v.io. 180 IN TXT "a"`))
|
|
||||||
Eventually(digSession, 1).Should(Exit(0))
|
Eventually(digSession, 1).Should(Exit(0))
|
||||||
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT delete.b.k-v.io. \? \["a"\]`))
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT delete.b.k-v.io. \? nil, SOA delete.b.k-v.io. briancunnie.gmail.com. 2022020800 900 900 1800 180`))
|
||||||
|
})
|
||||||
|
It(`the DELETE on a non-existent key behaves the same as the DELETE on an existing key`, func() {
|
||||||
|
digArgs = "@localhost delete.b.k-v.io txt -p " + strconv.Itoa(port)
|
||||||
|
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
|
||||||
|
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Eventually(digSession, 1).Should(Exit(0))
|
||||||
|
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT delete.b.k-v.io. \? nil, SOA delete.b.k-v.io. briancunnie.gmail.com. 2022020800 900 900 1800 180`))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When(`a record for an "_acme-challenge" domain is queried`, func() {
|
When(`a record for an "_acme-challenge" domain is queried`, func() {
|
||||||
|
@@ -88,7 +88,6 @@ 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.
|
||||||
// But `Customizations` is a true global variable.
|
|
||||||
var (
|
var (
|
||||||
ipv4REDots = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))($|[.-])`)
|
ipv4REDots = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))($|[.-])`)
|
||||||
ipv4REDashes = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?[0-9])?[0-9])-){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))($|[.-])`)
|
ipv4REDashes = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?[0-9])?[0-9])-){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))($|[.-])`)
|
||||||
@@ -116,7 +115,15 @@ var (
|
|||||||
VersionDate = "0001/01/01-99:99:99-0800"
|
VersionDate = "0001/01/01-99:99:99-0800"
|
||||||
VersionGitHash = "cafexxx"
|
VersionGitHash = "cafexxx"
|
||||||
|
|
||||||
MetricsBufferSize = 100
|
MetricsBufferSize = 100 // big enough to run our tests, and small enough to prevent DNS amplification attacks
|
||||||
|
|
||||||
|
// etcdContextTimeout — the duration (context) that we wait for etcd to get back to us
|
||||||
|
// - etcd queries on the nameserver take as long as 482 milliseconds on the "slow" server, 247 on the "fast"
|
||||||
|
// - round-trip time from my house in San Francisco to ns-azure in Singapore is 190 milliseconds
|
||||||
|
// - time between queries with `dig` on my macOS Monterey is 5000 milliseconds, three queries before giving up
|
||||||
|
// - quadruple the headroom for queries 4 x 482 = 1928, should still leave enough room to get answer back
|
||||||
|
// within the 5000 milliseconds
|
||||||
|
etcdContextTimeout = 1928 * time.Millisecond
|
||||||
|
|
||||||
TxtKvCustomizations = KvCustomizations{}
|
TxtKvCustomizations = KvCustomizations{}
|
||||||
Customizations = DomainCustomizations{
|
Customizations = DomainCustomizations{
|
||||||
@@ -652,6 +659,7 @@ func NameToA(fqdnString string) []dnsmessage.AResource {
|
|||||||
ipv4address := net.ParseIP(match).To4()
|
ipv4address := net.ParseIP(match).To4()
|
||||||
// We shouldn't reach here because `match` should always be valid, but we're not optimists
|
// We shouldn't reach here because `match` should always be valid, but we're not optimists
|
||||||
if ipv4address == nil {
|
if ipv4address == nil {
|
||||||
|
// e.g. "ubuntu20.04.235.249.181-notify.sslip.io."
|
||||||
log.Printf("----> Should be valid A but isn't: %s\n", fqdn) // TODO: delete this
|
log.Printf("----> Should be valid A but isn't: %s\n", fqdn) // TODO: delete this
|
||||||
return []dnsmessage.AResource{}
|
return []dnsmessage.AResource{}
|
||||||
}
|
}
|
||||||
@@ -864,7 +872,7 @@ func (x *Xip) getKv(key string) ([]dnsmessage.TXTResource, error) {
|
|||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
|
ctx, cancel := context.WithTimeout(context.Background(), etcdContextTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
resp, err := x.Etcd.Get(ctx, key)
|
resp, err := x.Etcd.Get(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -888,7 +896,7 @@ func (x *Xip) putKv(key, value string) ([]dnsmessage.TXTResource, error) {
|
|||||||
}
|
}
|
||||||
return TxtKvCustomizations[key], nil
|
return TxtKvCustomizations[key], nil
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
|
ctx, cancel := context.WithTimeout(context.Background(), etcdContextTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, err := x.Etcd.Put(ctx, key, value)
|
_, err := x.Etcd.Put(ctx, key, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -899,28 +907,18 @@ func (x *Xip) putKv(key, value string) ([]dnsmessage.TXTResource, error) {
|
|||||||
|
|
||||||
func (x *Xip) deleteKv(key string) ([]dnsmessage.TXTResource, error) {
|
func (x *Xip) deleteKv(key string) ([]dnsmessage.TXTResource, error) {
|
||||||
if x.isEtcdNil() {
|
if x.isEtcdNil() {
|
||||||
if deletedKey, ok := TxtKvCustomizations[key]; ok {
|
if _, ok := TxtKvCustomizations[key]; ok {
|
||||||
delete(TxtKvCustomizations, key)
|
delete(TxtKvCustomizations, key)
|
||||||
return deletedKey, nil
|
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
|
ctx, cancel := context.WithTimeout(context.Background(), etcdContextTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
getResp, err := x.Etcd.Get(ctx, key) // is the key set?
|
_, err := x.Etcd.Delete(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`couldn't GET "%s": %w`, key, err)
|
return nil, fmt.Errorf("couldn't DELETE (key %s): %w", key, err)
|
||||||
}
|
}
|
||||||
if len(getResp.Kvs) == 0 { // nothing to delete
|
return nil, nil
|
||||||
return []dnsmessage.TXTResource{}, nil
|
|
||||||
}
|
|
||||||
// the key is set; we need to delete it
|
|
||||||
value := string(getResp.Kvs[0].Value)
|
|
||||||
_, err = x.Etcd.Delete(ctx, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("couldn't DELETE (%s: %s): %w", key, value, err)
|
|
||||||
}
|
|
||||||
return []dnsmessage.TXTResource{{[]string{value}}}, 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
|
||||||
@@ -1174,7 +1172,7 @@ func clientv3New(etcdEndpoint string) (*clientv3.Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Let's do a query to determine if etcd is really, truly there
|
// Let's do a query to determine if etcd is really, truly there
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
|
ctx, cancel := context.WithTimeout(context.Background(), etcdContextTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, err = etcdCli.Get(ctx, "some-silly-key, doesn't matter if it exists")
|
_, err = etcdCli.Get(ctx, "some-silly-key, doesn't matter if it exists")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -182,7 +182,7 @@ var _ = Describe("Xip", func() {
|
|||||||
Entry("getting that value → that value", "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("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("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("deleting that value → empty array", "DelETe.my-key.k-v.io.", []string{}),
|
||||||
Entry("getting that deleted value → empty array", "my-key.k-v.io.", []string{}),
|
Entry("getting that deleted value → empty array", "my-key.k-v.io.", []string{}),
|
||||||
// errors
|
// errors
|
||||||
Entry("getting a non-existent key → empty array", "nonexistent.k-v.io.", []string{}),
|
Entry("getting a non-existent key → empty array", "nonexistent.k-v.io.", []string{}),
|
||||||
|
Reference in New Issue
Block a user