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

feat: tailscale integration #198

Merged
merged 2 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
141 changes: 99 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Wishlist

<!--toc:start-->

- [Wishlist](#wishlist)
- [Installation](#installation)
- [Usage](#usage)
- [CLI](#cli)
- [Remote](#remote)
- [Local](#local)
- [Library](#library)
- [Auth](#auth)
- [Local mode](#local-mode)
- [Server mode](#server-mode)
- [Agent forwarding example](#agent-forwarding-example)
- [Discovery](#discovery)
- [Tailscale](#tailscale)
- [Zeroconf/Avahi/mDNS/Bonjour](#zeroconfavahimdnsbonjour)
- [SRV records](#srv-records)
- [Hints](#hints)
- [Running it](#running-it)
- [Using the binary](#using-the-binary)
- [Using Docker](#using-docker)
- [Supported SSH Options](#supported-ssh-options)
- [Acknowledgments](#acknowledgments)
- [Feedback](#feedback)
- [License](#license)

<!--toc:end-->

<p>
<a href="https://github.com/charmbracelet/wishlist/releases"><img src="https://img.shields.io/github/release/charmbracelet/wishlist.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/github.com/charmbracelet/wishlist?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
Expand Down Expand Up @@ -144,6 +172,75 @@ Host wishlist
UserKnownHostsFile /dev/null
```

## Discovery

Wishlist can discover endpoints using Zeroconf, SRV Records, and [Tailscale][].

You can find a brief explanation and examples of all of them bellow.

Run `wishlist --help` to see all the options.

[Tailscale]: http://tailscale.com

### Tailscale

You can configure Wishlist to find all nodes in your **tailnet** and add them
as endpoints:

```bash
wishlist --tailscale.net=your_tailnet_name --tailscale.key=tskey-api-abc123...
```

You can use the [Hints](#Hints) to change the connection settings.

### Zeroconf/Avahi/mDNS/Bonjour

You can enable this using the `--zeroconf.enabled` flag:

```bash
wishlist --zeroconf.enabled
```

Optionally, you can also specify a timeout with `--zeroconf.timeout` and, which
domain to look for with `--zeroconf.domain`.

Wishlist will look for `_ssh._tcp` services in the given domain.

You can use the [Hints](#Hints) to change the connection settings.

### SRV records

You can set Wishlist up to find nodes from DNS `SRV` records:

```bash
wishlist --srv.domain example.com
```

By default, Wishlist will set the name of the endpoint to the `SRV` target.
You can, however, customize that with a `TXT` record in the following format:

```txt
wishlist.name full.address:22=thename
```

So, in this case, a `SRV` record pointing to `full.address` on port `22` will
get the name `thename`.

### Hints

You can use the `hints` key in the YAML configuration file to hint settings into
discovered endpoints.

Check the [example configuration file](/_example/config.yaml) to learn
what options are available.

If you're using a SSH configuration file as the Wishlist configuration file,
it'll try to match the hosts with the rules in the given configuration.
Otherwise, the services will simply be added to the list.

The difference is that if a hints themselves won't show in the TUI, as of hosts
in the SSH configuration will.

## Running it

Wishlist will read and store all its information in a `.wishlist` folder in the
Expand Down Expand Up @@ -185,47 +282,6 @@ run `wishlist` and get it running right away. It also means that if you don't
want that, you can pass a path to `-config`, and it can be either a YAML, or a
SSH config file.

### Zeroconf/Avahi/mDNS/Bonjour

Wishlist can also discover services using mDNS, to do so, run it with
`--zeroconf.enabled`.
Optionally, you can also specify a timeout with `--zeroconf.timeout` and, which
domain to look for with `--zeroconf.domain`.

Wishlist will look for `_ssh._tcp` services in the given domain.
If you're using a SSH configuration file as the Wishlist configuration file,
it'll try to match the hosts with the rules in the given configuration.
Otherwise, the services will simply be added to the list.

Run `wishlist --help` to see all the options.

### SRV records

Wishlist can also find nodes from DNS `SRV` records, on one or more domains.

Run `wishlist --srv.domain {your domain}` to get started. You can repeat the
flag for multiple domains.

By default, Wishlist will set the name of the endpoint to the `SRV` target.
You can, however, customize that with a `TXT` record in the following format:

```
wishlist.name full.address:22=thename
```

So, in this case, a `SRV` record pointing to `full.address` on port `22` will get
the name `thename`.

Run `wishlist --help` to see all the options.

### Hints

You can use the `hints` key in the YAML configuration file to hint settings into
discovered endpoints.

Check the [example configuration file](/_example/config.yaml) to learn
what options are available.

### Using the binary

```sh
Expand Down Expand Up @@ -283,4 +339,5 @@ Part of [Charm](https://charm.sh).

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>

Charm 热爱开源 • Charm loves open source
<!--prettier-ignore-->
Charm热爱开源 • Charm loves open source
15 changes: 15 additions & 0 deletions cmd/wishlist/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/charmbracelet/wishlist"
"github.com/charmbracelet/wishlist/srv"
"github.com/charmbracelet/wishlist/sshconfig"
"github.com/charmbracelet/wishlist/tailscale"
"github.com/charmbracelet/wishlist/zeroconf"
"github.com/gobwas/glob"
"github.com/hashicorp/go-multierror"
Expand Down Expand Up @@ -175,6 +176,8 @@ var (
zeroconfEnabled bool
zeroconfDomain string
zeroconfTimeout time.Duration
tailscaleNet string
tailscaleKey string
)

func init() {
Expand All @@ -185,6 +188,8 @@ func init() {
rootCmd.PersistentFlags().StringVar(&zeroconfDomain, "zeroconf.domain", "", "Domain to use with zeroconf service discovery")
rootCmd.PersistentFlags().DurationVar(&zeroconfTimeout, "zeroconf.timeout", time.Second, "How long should zeroconf keep searching for hosts")
rootCmd.PersistentFlags().StringSliceVar(&srvDomains, "srv.domain", nil, "SRV domains to discover endpoints")
rootCmd.PersistentFlags().StringVar(&tailscaleNet, "tailscale.net", "", "Tailscale tailnet name")
rootCmd.PersistentFlags().StringVar(&tailscaleKey, "tailscale.key", os.Getenv("TAILSCALE_KEY"), "Tailscale tailnet name [$TAILSCALE_KEY]")
rootCmd.AddCommand(serverCmd, manCmd)
}

Expand Down Expand Up @@ -300,6 +305,16 @@ func getConfigFile(path string, seed []*wishlist.Endpoint) (wishlist.Config, err

func getSeedEndpoints() ([]*wishlist.Endpoint, error) {
var seed []*wishlist.Endpoint
if tailscaleNet != "" {
if tailscaleKey == "" {
return nil, fmt.Errorf("missing tailscale.key")
}
endpoints, err := tailscale.Endpoints(tailscaleNet, tailscaleKey)
if err != nil {
return nil, err //nolint: wrapcheck
}
seed = append(seed, endpoints...)
}
if zeroconfEnabled {
endpoints, err := zeroconf.Endpoints(zeroconfDomain, zeroconfTimeout)
if err != nil {
Expand Down
59 changes: 59 additions & 0 deletions tailscale/tailscale.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package tailscale

import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"

"github.com/charmbracelet/log"
"github.com/charmbracelet/wishlist"
)

// Endpoints returns the found endpoints from tailscale.
func Endpoints(tailnet, key string) ([]*wishlist.Endpoint, error) {
log.Info("discovering from tailscale", "tailnet", tailnet)
req, err := http.NewRequest(
http.MethodGet,
fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/devices", tailnet),
nil,
)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

defer func() { _ = resp.Body.Close() }()
bts, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var devices struct {
Devices []device `json:"devices"`
}
if err := json.Unmarshal(bts, &devices); err != nil {
return nil, err
}

endpoints := make([]*wishlist.Endpoint, 0, len(devices.Devices))
for _, device := range devices.Devices {
endpoints = append(endpoints, &wishlist.Endpoint{
Name: device.Hostname,
Address: net.JoinHostPort(device.Addresses[0], "22"),
caarlos0 marked this conversation as resolved.
Show resolved Hide resolved
})
}
return endpoints, nil
}

type device struct {
ID string `json:"id"`
Addresses []string `json:"addresses"`
Authorized bool `json:"Authorized"`
Hostname string `json:"hostname"`
}