Skip to content

Commit

Permalink
tailscale: support split DNS endpoints (#78)
Browse files Browse the repository at this point in the history
Support the GET/PATCH/PUT `/api/v2/tailnet/{tailnetID}/dns/split-dns`
endpoints for reading, updating, and replacing split DNS settings for a
given tailnet respectively.

Updates tailscale/corp#19483

Signed-off-by: Mario Minardi <mario@tailscale.com>
  • Loading branch information
mpminardi committed Apr 29, 2024
1 parent 0299382 commit 488fd69
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 1 deletion.
64 changes: 63 additions & 1 deletion tailscale/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"time"

"github.com/tailscale/hujson"

"golang.org/x/oauth2/clientcredentials"
)

Expand Down Expand Up @@ -333,6 +332,69 @@ func (c *Client) DNSNameservers(ctx context.Context) ([]string, error) {
return resp["dns"], nil
}

// SplitDnsRequest is a map from domain names to a list of nameservers.
type SplitDnsRequest map[string][]string

// SplitDnsResponse is a map from domain names to a list of nameservers.
type SplitDnsResponse SplitDnsRequest

// UpdateSplitDNS updates the split DNS settings for a tailnet using the
// provided SplitDnsRequest object. This is a PATCH operation that performs
// partial updates of the underlying data structure.
//
// Mapping a domain to a nil slice in the request will unset the nameservers
// associated with that domain. Values provided for domains will overwrite the
// current value associated with the domain. Domains not included in the request
// will remain unchanged.
func (c *Client) UpdateSplitDNS(ctx context.Context, request SplitDnsRequest) (SplitDnsResponse, error) {
const uriFmt = "/api/v2/tailnet/%v/dns/split-dns"

req, err := c.buildRequest(ctx, http.MethodPatch, fmt.Sprintf(uriFmt, c.tailnet), requestBody(request))
if err != nil {
return nil, err
}

var resp SplitDnsResponse
if err = c.performRequest(req, &resp); err != nil {
return nil, err
}

return resp, nil
}

// SetSplitDNS sets the split DNS settings for a tailnet using the provided
// SplitDnsRequest object. This is a PUT operation that fully replaces the underlying
// data structure.
//
// Passing in an empty SplitDnsRequest will unset all split DNS mappings for the tailnet.
func (c *Client) SetSplitDNS(ctx context.Context, request SplitDnsRequest) error {
const uriFmt = "/api/v2/tailnet/%v/dns/split-dns"

req, err := c.buildRequest(ctx, http.MethodPut, fmt.Sprintf(uriFmt, c.tailnet), requestBody(request))
if err != nil {
return err
}

return c.performRequest(req, nil)
}

// SplitDNS retrieves the split DNS configuration for a tailnet.
func (c *Client) SplitDNS(ctx context.Context) (SplitDnsResponse, error) {
const uriFmt = "/api/v2/tailnet/%v/dns/split-dns"

req, err := c.buildRequest(ctx, http.MethodGet, fmt.Sprintf(uriFmt, c.tailnet))
if err != nil {
return nil, err
}

var resp SplitDnsResponse
if err = c.performRequest(req, &resp); err != nil {
return nil, err
}

return resp, nil
}

type (
// ACL contains the schema for a tailnet policy file. More details: https://tailscale.com/kb/1018/acls/
ACL struct {
Expand Down
65 changes: 65 additions & 0 deletions tailscale/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,24 @@ func TestClient_DNSSearchPaths(t *testing.T) {
assert.Equal(t, expectedPaths["searchPaths"], paths)
}

func TestClient_SplitDNS(t *testing.T) {
t.Parallel()

client, server := NewTestHarness(t)
server.ResponseCode = http.StatusOK

expectedNameservers := tailscale.SplitDnsResponse{
"example.com": {"1.1.1.1", "1.2.3.4"},
}

server.ResponseBody = expectedNameservers
nameservers, err := client.SplitDNS(context.Background())
assert.NoError(t, err)
assert.Equal(t, http.MethodGet, server.Method)
assert.Equal(t, "/api/v2/tailnet/example.com/dns/split-dns", server.Path)
assert.Equal(t, expectedNameservers, nameservers)
}

func TestClient_SetDNSNameservers(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -652,6 +670,53 @@ func TestClient_SetDNSSearchPaths(t *testing.T) {
assert.EqualValues(t, paths, body["searchPaths"])
}

func TestClient_UpdateSplitDNS(t *testing.T) {
t.Parallel()

client, server := NewTestHarness(t)
server.ResponseCode = http.StatusOK

nameservers := []string{"1.1.2.1", "3.3.3.4"}
request := tailscale.SplitDnsRequest{
"example.com": nameservers,
}

expectedNameservers := tailscale.SplitDnsResponse{
"example.com": nameservers,
}
server.ResponseBody = expectedNameservers

resp, err := client.UpdateSplitDNS(context.Background(), request)
assert.NoError(t, err)
assert.Equal(t, http.MethodPatch, server.Method)
assert.Equal(t, "/api/v2/tailnet/example.com/dns/split-dns", server.Path)

body := make(tailscale.SplitDnsResponse)
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &body))
assert.EqualValues(t, nameservers, body["example.com"])
assert.Equal(t, expectedNameservers, resp)
}

func TestClient_SetSplitDNS(t *testing.T) {
t.Parallel()

client, server := NewTestHarness(t)
server.ResponseCode = http.StatusOK

nameservers := []string{"1.1.2.1", "3.3.3.4"}
request := tailscale.SplitDnsRequest{
"example.com": nameservers,
}

assert.NoError(t, client.SetSplitDNS(context.Background(), request))
assert.Equal(t, http.MethodPut, server.Method)
assert.Equal(t, "/api/v2/tailnet/example.com/dns/split-dns", server.Path)

body := make(tailscale.SplitDnsResponse)
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &body))
assert.EqualValues(t, nameservers, body["example.com"])
}

func TestClient_AuthorizeDevice(t *testing.T) {
t.Parallel()

Expand Down

0 comments on commit 488fd69

Please sign in to comment.