Skip to content

Commit

Permalink
Simplify and test the code base (#5)
Browse files Browse the repository at this point in the history
This change drastically simplifies the layout of the code by moving all of the
internal packages into either main or the tailscalesd package itself.

It also improves confidence in the code by expanding test coverage from ~nothing
to most of the public API code, and some small bits of the local API code.
Functionally, the only change is the ability to specify the path to the local
API unix socket.
  • Loading branch information
cfunkhouser committed Apr 6, 2022
2 parents b527d05 + 89990af commit af6a763
Show file tree
Hide file tree
Showing 13 changed files with 963 additions and 319 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ https://prometheus.io/docs/prometheus/latest/http_sd/
## Usage

The `tailscalesd` server is very simple. It serves the SD payload at `/` on its
HTTP server. It respects four configuration parameters, each of which may be
specified as a flag or an environment variable.
HTTP server. It respects the following configuration parameters, each of which
may be specified as a flag or an environment variable.

- `-address` / `ADDRESS` is the host:port on which to serve TailscaleSD.
Defaults to `0.0.0.0:9242`.
- `-localapi` / `TAILSCALE_USE_LOCAL_API` instructs TailscaleSD to use the
`tailscaled`-exported local API for discovery.
- `-localapi_socket` / `TAILSCALE_LOCAL_API_SOCKET` is the path to the Unix
domain socket over which `tailscaled` serves the local API.
- `-poll` / `TAILSCALE_API_POLL_LIMIT` is the limit of how frequently the
Tailscale API may be polled. Cached results are served between intervals.
Defaults to 5 minutes. Also applies to local API.
- `-localapi` / `TAILSCALE_USE_LOCAL_API` instructs TailscaleSD to use the
`tailscaled`-exported local API for discovery.
- `-tailnet` / `TAILNET` is the name of the tailnet to enumerate. Required
when using the public API.
- `-token` / `TAILSCALE_API_TOKEN` is a Tailscale API token with appropriate
Expand Down
53 changes: 40 additions & 13 deletions cmd/tailscalesd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import (
"time"

"github.com/cfunkhouser/tailscalesd"
"github.com/cfunkhouser/tailscalesd/internal/logwriter"
)

var (
address string = "0.0.0.0:9242"
token string
tailnet string
printVer bool
pollLimit time.Duration = time.Minute * 5
useLocalAPI bool
address string = "0.0.0.0:9242"
token string
tailnet string
printVer bool
pollLimit time.Duration = time.Minute * 5
useLocalAPI bool
localAPISocket string = tailscalesd.PublicAPIHost

// Version of tailscalesd. Set at build time to something meaningful.
Version = "development"
Expand Down Expand Up @@ -58,11 +58,24 @@ func defineFlags() {
flag.DurationVar(&pollLimit, "poll", durationEnvVarWithDefault("TAILSCALE_API_POLL_LIMIT", pollLimit), "Max frequency with which to poll the Tailscale API. Cached results are served between intervals.")
flag.BoolVar(&printVer, "version", false, "Print the version and exit.")
flag.BoolVar(&useLocalAPI, "localapi", boolEnvVarWithDefault("TAILSCALE_USE_LOCAL_API", false), "Use the Tailscale local API exported by the local node's tailscaled")
flag.StringVar(&localAPISocket, "localapi_socket", envVarWithDefault("TAILSCALE_LOCAL_API_SOCKET", localAPISocket), "Unix Domain Socket to use for communication with the local tailscaled API.")
}

type logWriter struct {
TZ *time.Location
Format string
}

func (w *logWriter) Write(data []byte) (int, error) {
return fmt.Printf("%v %v", time.Now().In(w.TZ).Format(w.Format), string(data))
}

func main() {
log.SetFlags(0)
log.SetOutput(logwriter.Default())
log.SetOutput(&logWriter{
TZ: time.UTC,
Format: time.RFC3339,
})

defineFlags()
flag.Parse()
Expand All @@ -73,18 +86,32 @@ func main() {
}

if !useLocalAPI && (token == "" || tailnet == "") {
fmt.Println("Both -token and -tailnet are required when using the public API")
if _, err := fmt.Fprintln(os.Stderr, "Both -token and -tailnet are required when using the public API"); err != nil {
panic(err)
}
flag.Usage()
return
}

var d tailscalesd.Discoverer
if useLocalAPI && localAPISocket == "" {
if _, err := fmt.Fprintln(os.Stderr, "-localapi_socket must not be empty when using the local API."); err != nil {
panic(err)
}
flag.Usage()
return
}

var ts tailscalesd.Discoverer
if useLocalAPI {
d = tailscalesd.New(tailscalesd.UsingLocalAPI(), tailscalesd.WithRateLimit(pollLimit))
ts = tailscalesd.LocalAPI(tailscalesd.LocalAPISocket)
} else {
d = tailscalesd.New(tailscalesd.UsingPublicAPI(tailnet, token), tailscalesd.WithRateLimit(pollLimit))
ts = tailscalesd.PublicAPI(tailnet, token)
}
ts = &tailscalesd.RateLimitedDiscoverer{
Wrap: ts,
Frequency: pollLimit,
}
http.Handle("/", tailscalesd.Export(d, time.Minute*5))
http.Handle("/", tailscalesd.Export(ts))
log.Printf("Serving Tailscale service discovery on %q", address)
log.Print(http.ListenAndServe(address, nil))
log.Print("Done")
Expand Down
27 changes: 0 additions & 27 deletions internal/logwriter/logwriter.go

This file was deleted.

55 changes: 0 additions & 55 deletions internal/tailscale/public/public.go

This file was deleted.

19 changes: 0 additions & 19 deletions internal/tailscale/tailscale.go

This file was deleted.

75 changes: 42 additions & 33 deletions internal/tailscale/local/local.go → localapi.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// Package local is a client for the Tailscale local API, which is exported by
// tailscaled. It has only the functionality needed for tailscalesd. You should
// not rely on its API for anything else.
package local
package tailscalesd

import (
"context"
Expand All @@ -10,23 +7,19 @@ import (
"fmt"
"net"
"net/http"
"time"

"inet.af/netaddr"

"github.com/cfunkhouser/tailscalesd/internal/tailscale"
)

// unixDialer is a DialContext allowing HTTP communication via a unix domain
// socket.
func unixDialer(socket string) func(context.Context, string, string) (net.Conn, error) {
return func(ctx context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socket)
}
}
// LocalAPISocket is the path to the Unix domain socket on which tailscaled
// listens locally.
const LocalAPISocket = "/run/tailscale/tailscaled.sock"

// interstingStatusSubset is a json-decodeable subset of the Status struct
// served by the Tailscale local API. This is done to prevent pulling the Tailscale code base and its dependencies into this module.
// The fields were borrowed from version 1.22.2. For field details, see:
// served by the Tailscale local API. This is done to prevent pulling the
// Tailscale code base and its dependencies into this module. The fields were
// borrowed from version 1.22.2. For field details, see:
// https://pkg.go.dev/tailscale.com@v1.22.2/ipn/ipnstate?utm_source=gopls#Status
type interestingStatusSubset struct {
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Expand All @@ -45,14 +38,13 @@ type interestingPeerStatusSubset struct {
Tags []string `json:",omitempty"`
}

// API client for the Tailscale local API.
type API struct {
type localAPIClient struct {
client *http.Client
}

var ErrFailedRequest = errors.New("failed localapi call")
var errFailedLocalAPIRequest = errors.New("failed local API request")

func (a *API) status(ctx context.Context) (interestingStatusSubset, error) {
func (a *localAPIClient) status(ctx context.Context) (interestingStatusSubset, error) {
var status interestingStatusSubset
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost/localapi/v0/status", nil)
if err != nil {
Expand All @@ -64,7 +56,7 @@ func (a *API) status(ctx context.Context) (interestingStatusSubset, error) {
}

if (resp.StatusCode / 100) != 2 {
return status, fmt.Errorf("%w: %v", ErrFailedRequest, resp.Status)
return status, fmt.Errorf("%w: %v", errFailedLocalAPIRequest, resp.Status)
}
defer resp.Body.Close()

Expand All @@ -74,27 +66,25 @@ func (a *API) status(ctx context.Context) (interestingStatusSubset, error) {
return status, nil
}

func translatePeerToDevice(p *interestingPeerStatusSubset, d *tailscale.Device) {
func translatePeerToDevice(p *interestingPeerStatusSubset, d *Device) {
for i := range p.TailscaleIPs {
d.Addresses = append(d.Addresses, p.TailscaleIPs[i].String())
}

// Assumes that if the peer is listed in localapi, it is authorized enough.
d.Authorized = true
d.API = "localhost"
d.Authorized = true // localapi returned peer; assume it's authorized enough
d.Hostname = p.HostName
d.ID = fmt.Sprintf("%v", p.ID)
d.ID = p.ID
d.OS = p.OS
d.Tags = p.Tags[:]
}

// Devices reported by the Tailscale local API as peers of the local host.
func (a *API) Devices(ctx context.Context) ([]tailscale.Device, error) {
func (a *localAPIClient) Devices(ctx context.Context) ([]Device, error) {
status, err := a.status(ctx)
if err != nil {
return nil, err
}
devices := make([]tailscale.Device, len(status.Peer))
devices := make([]Device, len(status.Peer))
var i int
for _, peer := range status.Peer {
translatePeerToDevice(peer, &devices[i])
Expand All @@ -103,12 +93,31 @@ func (a *API) Devices(ctx context.Context) ([]tailscale.Device, error) {
return devices, nil
}

func New(socket string) *API {
return &API{
client: &http.Client{
Transport: &http.Transport{
DialContext: unixDialer(socket),
},
type dialContext func(context.Context, string, string) (net.Conn, error)

func unixSocketDialer(socket string) dialContext {
return func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", socket)
}
}

func defaultHTTPClientWithDialer(dc dialContext) *http.Client {
return &http.Client{
Timeout: time.Second * 10,
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
}).Dial,
DialContext: dc,
TLSHandshakeTimeout: 5 * time.Second,
},
}
}

// LocalAPI Discoverer interrogates the Tailscale localapi for peer devices.
func LocalAPI(socket string) Discoverer {
return &localAPIClient{
client: defaultHTTPClientWithDialer(unixSocketDialer(socket)),
}
}
44 changes: 44 additions & 0 deletions localapi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package tailscalesd

import (
"testing"

"github.com/google/go-cmp/cmp"
"inet.af/netaddr"
)

func TestTranslatePeerToDevice(t *testing.T) {
want := Device{
Addresses: []string{
"100.2.3.4",
"fd7a::1234",
},
API: "localhost",
Authorized: true,
Hostname: "somethingclever",
ID: "id",
OS: "beos",
Tags: []string{
"tag:foo",
"tag:bar",
},
}
var got Device
translatePeerToDevice(&interestingPeerStatusSubset{
ID: "id",
HostName: "somethingclever",
DNSName: "this is currently ignored",
OS: "beos",
TailscaleIPs: []netaddr.IP{
netaddr.MustParseIP("100.2.3.4"),
netaddr.MustParseIP("fd7a::1234"),
},
Tags: []string{
"tag:foo",
"tag:bar",
},
}, &got)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("translatePeerToDevice: mismatch (-got, +want):\n%v", diff)
}
}
Loading

0 comments on commit af6a763

Please sign in to comment.