Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify and test the code base. #5

Merged
merged 5 commits into from
Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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