Skip to content

Commit

Permalink
MVP
Browse files Browse the repository at this point in the history
  • Loading branch information
aaanh committed Apr 4, 2024
1 parent 42725dd commit 9d1243e
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 45 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,21 @@ Sync your **Tail**scale devices to Cloud**flare** DNS.
The functionality is based on this documentation on Tailscale: https://tailscale.com/kb/1054/dns?q=subdomain#using-a-public-dns-subdomain

It is basically taking the Tailscale IP addresses and put them under a subdomain A record on the DNS provider, which is Cloudflare in our case.

## Usage

### Run directly from source

1. Prerequisites:

- `go` >= v1.21
- `make`

2. Clone this repository

```
git clone https://github.com/aaanh/tailflare
```

3. Go would hopefully install the needed dependencies on first run
4. Run `make run`
1 change: 1 addition & 0 deletions src/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
TAILSCALE_API_KEY=xxx
CLOUDFLARE_API_KEY=xxx
CLOUDFLARE_ZONE_ID=xxx
TAILNET_ORG=xxx
59 changes: 46 additions & 13 deletions src/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ func updateStates(config *Config) {
} else {
config.States.TailnetOrgExist = false
}

if len(config.CloudflareZoneId) > 0 {
config.States.CloudflareZoneIdExist = true
} else {
config.States.CloudflareZoneIdExist = false
}
}

func configureTailscale(config *Config) {
Expand All @@ -35,6 +41,16 @@ func configureTailscale(config *Config) {
fmt.Printf("\n\n")
}

func configureTailnetOrg(config *Config) {
fmt.Println("Enter Tailnet organization")
fmt.Printf("Should be under the Organization section at https://login.tailscale.com/admin/settings/general\n\n")
fmt.Print("> ")
var tailnetOrg string
fmt.Scan(&tailnetOrg)
config.TailnetOrg = tailnetOrg
fmt.Printf("\n\n")
}

func configureCloudflare(config *Config) {
fmt.Println("Enter Cloudflare API key")
fmt.Printf("How to get your Cloudflare API key: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\n\n")
Expand All @@ -45,16 +61,26 @@ func configureCloudflare(config *Config) {
fmt.Printf("\n\n")
}

func configureTailnetOrg(config *Config) {
fmt.Println("Enter Tailnet organization")
fmt.Printf("Should be under the Organization section at https://login.tailscale.com/admin/settings/general\n\n")
func configureCloudflareZoneId(config *Config) {
fmt.Println("Enter Cloudflare Zone ID")
fmt.Printf("Log in to the dashboard and select the target domain. Zone ID should be under the API section\n\n")
fmt.Print("> ")
var tailnetOrg string
fmt.Scan(&tailnetOrg)
config.TailnetOrg = tailnetOrg
var cfZoneId string
fmt.Scan(&cfZoneId)
config.CloudflareZoneId = cfZoneId
fmt.Printf("\n\n")
}

func dryRun(config Config) any {
fmt.Printf("\n\nPerforming dry run and display what-if results.\n\n")
return nil
}

func performSync(config *Config) {
checkCloudflareToken(config)
addCloudflareDnsRecords(config, getTailscaleDevices(config))
}

func menu(cfg Config) int {
menuOptions := map[int]string{
1: fmt.Sprintf("Configure Tailscale API key (%s)", func() string {
Expand All @@ -64,23 +90,30 @@ func menu(cfg Config) int {
return "Not added"
}
}()),
2: fmt.Sprintf("Configure Cloudflare API key (%s)", func() string {
2: fmt.Sprintf("Configure Tailnet Organization (%s)", func() string {
if cfg.States.TailnetOrgExist {
return cfg.TailnetOrg
} else {
return "Not added"
}
}()),
3: fmt.Sprintf("Configure Cloudflare API key (%s)", func() string {
if cfg.States.CloudflareKeyExist {
return "Added"
} else {
return "Not added"
}
}()),
3: fmt.Sprintf("Configure Tailnet Organization (%s)", func() string {
if cfg.States.TailnetOrgExist {
return cfg.TailnetOrg
4: fmt.Sprintf("Configure Cloudflare Zone ID (%s)", func() string {
if cfg.States.CloudflareZoneIdExist {
return cfg.CloudflareZoneId
} else {
return "Not added"
}
}()),
4: "Dry run (What-If)",
5: "Perform Sync",
6: "Exit",
5: "Dry run (What-If)",
6: "Perform Sync",
7: "Exit",
}

// Solve the misordered printing by sorting the keys in the map
Expand Down
57 changes: 38 additions & 19 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,21 @@ func displayHeader(version string) {
fmt.Println()
}

func dryRun(config Config) any {
fmt.Printf("\n\nPerforming dry run and display what-if results.\n\n")
return nil
}

func runSync(config *Config) {
getTailscaleDevices(config)
}

func choiceHandler(choice int, config *Config) {
switch choice {
case 1:
configureTailscale(config)
case 2:
configureCloudflare(config)
case 3:
configureTailnetOrg(config)
case 3:
configureCloudflare(config)
case 4:
dryRun(*config)
configureCloudflareZoneId(config)
case 5:
runSync(config)
dryRun(*config)
case 6:
performSync(config)
case 7:
{
fmt.Println("\n=== Thanks for using Tailflare :) ===")
os.Exit(0)
Expand All @@ -59,17 +52,43 @@ func program(config *Config) {
}

func main() {
states := States{false, false, false}
states := States{false, false, false, false}

var version string
var tailscaleApiKey string
var cloudflareApiKey string
var tailnetOrg string
var cloudflareZoneId string

defer func() {
if err := recover(); err != nil {
fmt.Println("Panic occurred during environment variable load.")
fmt.Println(err)

version := "0.0.0-undefined"
tailscaleApiKey := ""
tailnetOrg := ""
cloudflareApiKey := ""
cloudflareZoneId := ""

config := Config{version, states,
Keys{tailscaleApiKey, cloudflareApiKey},
tailnetOrg, cloudflareZoneId}

program(&config)
}
}()
env.Load("./config.cfg", "./.env")
version := env.Get("version", "0.0.0-undefined")
tailscaleApiKey := env.Get("TAILSCALE_API_KEY", "")
cloudflareApiKey := env.Get("CLOUDFLARE_API_KEY", "")
tailnetOrg := env.Get("TAILNET_ORG", "")

version = env.Get("version", "0.0.0-undefined")
tailscaleApiKey = env.Get("TAILSCALE_API_KEY", "")
tailnetOrg = env.Get("TAILNET_ORG", "")
cloudflareApiKey = env.Get("CLOUDFLARE_API_KEY", "")
cloudflareZoneId = env.Get("CLOUDFLARE_ZONE_ID", "")

config := Config{version, states,
Keys{tailscaleApiKey, cloudflareApiKey},
tailnetOrg}
tailnetOrg, cloudflareZoneId}

program(&config)

Expand Down
24 changes: 17 additions & 7 deletions src/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@ type Keys struct {
}

type States struct {
TailscaleKeyExist bool
CloudflareKeyExist bool
TailnetOrgExist bool
TailscaleKeyExist bool
TailnetOrgExist bool
CloudflareKeyExist bool
CloudflareZoneIdExist bool
}

type Config struct {
Version string
States States
Keys Keys
TailnetOrg string
Version string
States States
Keys Keys
TailnetOrg string
CloudflareZoneId string
}

type Payload struct {
Content string `json:"content"`
Name string `json:"name"`
Proxied bool `json:"proxied"`
Type string `json:"type"`
Ttl int `json:"ttl"`
}
108 changes: 102 additions & 6 deletions src/utils.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"

"net/http"
)
Expand All @@ -18,25 +20,29 @@ type Devices struct {
Devices []Device `json:"devices"`
}
type Endpoints struct {
Devices string
TailscaleDevices string
CloudflareKeyCheck string
CloudflareAddRecord string
}

func generateEndpoints(tailnetOrg string) Endpoints {
func generateEndpoints(cfg *Config) Endpoints {
endpoints := Endpoints{
fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/devices", tailnetOrg),
fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/devices", cfg.TailnetOrg),
"https://api.cloudflare.com/client/v4/user/tokens/verify",
fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", cfg.CloudflareZoneId),
}

return endpoints
}

func getTailscaleDevices(config *Config) {
func getTailscaleDevices(config *Config) Devices {
fmt.Printf("\n> Step: Querying Tailscale devices\n\n")
client := &http.Client{}

// var devices Devices
endpoints := generateEndpoints(config.TailnetOrg)
endpoints := generateEndpoints(config)

req, _ := http.NewRequest("GET", endpoints.Devices, nil)
req, _ := http.NewRequest("GET", endpoints.TailscaleDevices, nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", config.Keys.TailscaleApiKey))

resp, err := client.Do(req)
Expand All @@ -60,4 +66,94 @@ func getTailscaleDevices(config *Config) {
fmt.Println()
}
fmt.Println()

return data
}

func checkCloudflareToken(config *Config) {
fmt.Println("> Checking Cloudflare token...")
client := &http.Client{}

endpoints := generateEndpoints(config)
req, _ := http.NewRequest("GET", endpoints.CloudflareKeyCheck, nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", config.Keys.CloudflareApiKey))

resp, err := client.Do(req)
if err != nil {
fmt.Println("Unable to perform Cloudflare API key check request")
}
body, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()

var data struct {
Result struct {
Status string
}
Success bool
Messages []struct {
Message string
}
}
err = json.Unmarshal(body, &data)
if err != nil {
fmt.Printf("Error unmarshalling response: %s", err)
fmt.Println()
}

fmt.Printf("\n\nCheck results:\n> key_status: %s\n> success: %t\n> msg: %s\n\n",
data.Result.Status, data.Success, data.Messages[0].Message)
}

func constructCloudflareDnsPayload(device Device, uri string) Payload {

deviceName := strings.Split(device.Name, ".")[0]

payload := Payload{
Content: device.Addresses[0],
Name: deviceName + "." + uri,
Proxied: false,
Type: "A",
Ttl: 3600,
}

return payload
}

func addCloudflareDnsRecords(config *Config, devices Devices) {
fmt.Println("Enter your domain URI (e.g. devices.my-domain.com)")
fmt.Printf("> ")
var uri string
fmt.Scanln(&uri)

client := &http.Client{}

endpoints := generateEndpoints(config)

fmt.Println("> NOW ADDING DEVICES...")

for idx, device := range devices.Devices {
fmt.Printf(">> Adding device %d\n", idx)
payload := constructCloudflareDnsPayload(device, uri)
marshalled, err := json.Marshal(payload)
if err != nil {
fmt.Println("Error marshaling JSON:", err)
return
}

req, _ := http.NewRequest("POST", endpoints.CloudflareAddRecord, bytes.NewReader(marshalled))

req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+config.Keys.CloudflareApiKey)

res, _ := client.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(res)
fmt.Println(string(body))
fmt.Println()
fmt.Println()
}

}

0 comments on commit 9d1243e

Please sign in to comment.