From e29cec759af2ef2561706e71ed480d5f83448014 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 7 Jun 2021 16:03:16 -0700 Subject: [PATCH] ipn/{ipnlocal,localapi}, control/controlclient: add SetDNS localapi Updates #1235 Signed-off-by: Brad Fitzpatrick --- control/controlclient/auto.go | 6 +++++ control/controlclient/client.go | 3 +++ control/controlclient/direct.go | 47 +++++++++++++++++++++++++++++++++ ipn/ipnlocal/local.go | 36 +++++++++++++++++++++++++ ipn/ipnlocal/state_test.go | 4 +++ ipn/localapi/localapi.go | 21 +++++++++++++++ 6 files changed, 117 insertions(+) diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go index 71aac5ff7..ad117e08a 100644 --- a/control/controlclient/auto.go +++ b/control/controlclient/auto.go @@ -716,3 +716,9 @@ func (c *Auto) TestOnlySetAuthKey(authkey string) { func (c *Auto) TestOnlyTimeNow() time.Time { return c.timeNow() } + +// SetDNS sends the SetDNSRequest request to the control plane server, +// requesting a DNS record be created or updated. +func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error { + return c.direct.SetDNS(ctx, req) +} diff --git a/control/controlclient/client.go b/control/controlclient/client.go index 403c4e5a7..c50169ada 100644 --- a/control/controlclient/client.go +++ b/control/controlclient/client.go @@ -74,4 +74,7 @@ type Client interface { // in a separate http request. It has nothing to do with the rest of // the state machine. UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) + // SetDNS sends the SetDNSRequest request to the control plane server, + // requesting a DNS record be created or updated. + SetDNS(context.Context, *tailcfg.SetDNSRequest) error } diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 1f884e2ce..23662f9ec 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -1211,3 +1211,50 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- } } } + +// SetDNS sends the SetDNSRequest request to the control plane server, +// requesting a DNS record be created or updated. +func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error { + c.mu.Lock() + serverKey := c.serverKey + c.mu.Unlock() + + if serverKey.IsZero() { + return errors.New("zero serverKey") + } + machinePrivKey, err := c.getMachinePrivKey() + if err != nil { + return fmt.Errorf("getMachinePrivKey: %w", err) + } + if machinePrivKey.IsZero() { + return errors.New("getMachinePrivKey returned zero key") + } + + bodyData, err := encode(req, &serverKey, &machinePrivKey) + if err != nil { + return err + } + body := bytes.NewReader(bodyData) + + u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().HexString()) + hreq, err := http.NewRequestWithContext(ctx, "POST", u, body) + if err != nil { + return err + } + res, err := c.httpc.Do(hreq) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != 200 { + msg, _ := ioutil.ReadAll(res.Body) + return fmt.Errorf("sign-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg))) + } + var setDNSRes struct{} // no fields yet + if err := decode(res, &setDNSRes, &serverKey, &machinePrivKey); err != nil { + c.logf("error decoding SetDNSResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err) + return fmt.Errorf("set-dns-response: %v", err) + } + + return nil +} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 0ee5de1f9..3fb0564c6 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2577,6 +2577,42 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) { return ret, nil } +// SetDNS adds a DNS record for the given domain name & TXT record +// value. +// +// It's meant for use with dns-01 ACME (LetsEncrypt) challenges. +// +// This is the low-level interface. Other layers will provide more +// friendly options to get HTTPS certs. +func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error { + req := &tailcfg.SetDNSRequest{ + Version: 1, + Type: "TXT", + Name: name, + Value: value, + } + + b.mu.Lock() + cc := b.cc + if prefs := b.prefs; prefs != nil { + req.NodeKey = tailcfg.NodeKey(prefs.Persist.PrivateNodeKey.Public()) + } + b.mu.Unlock() + if cc == nil { + return errors.New("not connected") + } + if req.NodeKey.IsZero() { + return errors.New("no nodekey") + } + if name == "" { + return errors.New("missing 'name'") + } + if value == "" { + return errors.New("missing 'value'") + } + return cc.SetDNS(ctx, req) +} + func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) { b.mu.Lock() defer b.mu.Unlock() diff --git a/ipn/ipnlocal/state_test.go b/ipn/ipnlocal/state_test.go index 7fef0c166..f2f2821e4 100644 --- a/ipn/ipnlocal/state_test.go +++ b/ipn/ipnlocal/state_test.go @@ -248,6 +248,10 @@ func (cc *mockControl) UpdateEndpoints(localPort uint16, endpoints []tailcfg.End cc.called("UpdateEndpoints") } +func (*mockControl) SetDNS(context.Context, *tailcfg.SetDNSRequest) error { + panic("unexpected SetDNS call") +} + // A very precise test of the sequence of function calls generated by // ipnlocal.Local into its controlclient instance, and the events it // produces upstream into the UI. diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index f8adb188b..00006384a 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -100,6 +100,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.serveBugReport(w, r) case "/localapi/v0/file-targets": h.serveFileTargets(w, r) + case "/localapi/v0/set-dns": + h.serveSetDNS(w, r) case "/": io.WriteString(w, "tailscaled\n") default: @@ -382,6 +384,25 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) { rp.ServeHTTP(w, outReq) } +func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) { + if !h.PermitWrite { + http.Error(w, "access denied", http.StatusForbidden) + return + } + if r.Method != "POST" { + http.Error(w, "want POST", 400) + return + } + ctx := r.Context() + err := h.b.SetDNS(ctx, r.FormValue("name"), r.FormValue("value")) + if err != nil { + writeErrorJSON(w, err) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(struct{}{}) +} + var dialPeerTransportOnce struct { sync.Once v *http.Transport