Skip to content

Commit

Permalink
cmd/connector-gen: add helper tool for wide app connector configurations
Browse files Browse the repository at this point in the history
connector-gen can initially generate connector ACL snippets and
advertise-routes flags for Github and AWS based on their public IP /
domain data.

Updates ENG-2425
Signed-off-by: James Tucker <james@tailscale.com>
  • Loading branch information
raggi committed Dec 15, 2023
1 parent 706e30d commit 3a635db
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 0 deletions.
15 changes: 15 additions & 0 deletions cmd/connector-gen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# connector-gen

Generate Tailscale app connector configuration details from third party data.

Tailscale app connectors are used to dynamically route traffic for domain names
via specific nodes on a tailnet. For larger upstream domains this may involve a
large number of domains or routes, and fully dynamic discovery may be slower or
involve more manual labor than ideal. This can be accelerated by
pre-configuration of the associated routes, based on data provided by the
target providers, which can be used to set precise `autoApprovers` routes, and
also to pre-populate the subnet routes via `--advertise-routes` avoiding
frequent routing reconfiguration that may otherwise occur while routes are
first being discovered and advertised by the connectors.


22 changes: 22 additions & 0 deletions cmd/connector-gen/advertise-routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package main

import (
"fmt"
"strings"

"go4.org/netipx"
)

func advertiseRoutes(set *netipx.IPSet) {
fmt.Println()
prefixes := set.Prefixes()
pfxs := make([]string, 0, len(prefixes))
for _, pfx := range prefixes {
pfxs = append(pfxs, pfx.String())
}
fmt.Printf("--advertise-routes=%s", strings.Join(pfxs, ","))
fmt.Println()
}
68 changes: 68 additions & 0 deletions cmd/connector-gen/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/netip"

"go4.org/netipx"
)

// See https://docs.aws.amazon.com/vpc/latest/userguide/aws-ip-ranges.html

type AWSMeta struct {
SyncToken string `json:"syncToken"`
CreateDate string `json:"createDate"`
Prefixes []struct {
IPPrefix string `json:"ip_prefix"`
Region string `json:"region"`
Service string `json:"service"`
NetworkBorderGroup string `json:"network_border_group"`
} `json:"prefixes"`
Ipv6Prefixes []struct {
Ipv6Prefix string `json:"ipv6_prefix"`
Region string `json:"region"`
Service string `json:"service"`
NetworkBorderGroup string `json:"network_border_group"`
} `json:"ipv6_prefixes"`
}

func aws() {
r, err := http.Get("https://ip-ranges.amazonaws.com/ip-ranges.json")
if err != nil {
log.Fatal(err)
}
defer r.Body.Close()

var aws AWSMeta
if err := json.NewDecoder(r.Body).Decode(&aws); err != nil {
log.Fatal(err)
}

var ips netipx.IPSetBuilder

for _, prefix := range aws.Prefixes {
ips.AddPrefix(netip.MustParsePrefix(prefix.IPPrefix))
}
for _, prefix := range aws.Ipv6Prefixes {
ips.AddPrefix(netip.MustParsePrefix(prefix.Ipv6Prefix))
}

set, err := ips.IPSet()
if err != nil {
log.Fatal(err)
}

fmt.Println(`"routes": [`)
for _, pfx := range set.Prefixes() {
fmt.Printf(`"%s": ["tag:connector"],%s`, pfx.String(), "\n")
}
fmt.Println(`]`)

advertiseRoutes(set)
}
34 changes: 34 additions & 0 deletions cmd/connector-gen/connector-gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

// connector-gen is a tool to generate app connector configuration and flags from service provider address data.
package main

import (
"fmt"
"os"
)

func help() {
fmt.Fprintf(os.Stderr, "Usage: %s [help|github|aws] [subcommand-arguments]\n", os.Args[0])
}

func main() {
if len(os.Args) < 2 {
help()
os.Exit(128)
}

switch os.Args[1] {
case "help", "-h", "--help":
help()
os.Exit(0)
case "github":
github()
case "aws":
aws()
default:
help()
os.Exit(128)
}
}
116 changes: 116 additions & 0 deletions cmd/connector-gen/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/netip"
"slices"
"strings"

"go4.org/netipx"
)

// See https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-githubs-ip-addresses

type GithubMeta struct {
VerifiablePasswordAuthentication bool `json:"verifiable_password_authentication"`
SSHKeyFingerprints struct {
Sha256Ecdsa string `json:"SHA256_ECDSA"`
Sha256Ed25519 string `json:"SHA256_ED25519"`
Sha256Rsa string `json:"SHA256_RSA"`
} `json:"ssh_key_fingerprints"`
SSHKeys []string `json:"ssh_keys"`
Hooks []string `json:"hooks"`
Web []string `json:"web"`
API []string `json:"api"`
Git []string `json:"git"`
GithubEnterpriseImporter []string `json:"github_enterprise_importer"`
Packages []string `json:"packages"`
Pages []string `json:"pages"`
Importer []string `json:"importer"`
Actions []string `json:"actions"`
Dependabot []string `json:"dependabot"`
Domains struct {
Website []string `json:"website"`
Codespaces []string `json:"codespaces"`
Copilot []string `json:"copilot"`
Packages []string `json:"packages"`
} `json:"domains"`
}

func github() {
r, err := http.Get("https://api.github.com/meta")
if err != nil {
log.Fatal(err)
}

var ghm GithubMeta

if err := json.NewDecoder(r.Body).Decode(&ghm); err != nil {
log.Fatal(err)
}
r.Body.Close()

var ips netipx.IPSetBuilder

var lists []string
lists = append(lists, ghm.Hooks...)
lists = append(lists, ghm.Web...)
lists = append(lists, ghm.API...)
lists = append(lists, ghm.Git...)
lists = append(lists, ghm.GithubEnterpriseImporter...)
lists = append(lists, ghm.Packages...)
lists = append(lists, ghm.Pages...)
lists = append(lists, ghm.Importer...)
lists = append(lists, ghm.Actions...)
lists = append(lists, ghm.Dependabot...)

for _, s := range lists {
ips.AddPrefix(netip.MustParsePrefix(s))
}

set, err := ips.IPSet()
if err != nil {
log.Fatal(err)
}

fmt.Println(`"routes": [`)
for _, pfx := range set.Prefixes() {
fmt.Printf(`"%s": ["tag:connector"],%s`, pfx.String(), "\n")
}
fmt.Println(`]`)

fmt.Println()

var domains []string
domains = append(domains, ghm.Domains.Website...)
domains = append(domains, ghm.Domains.Codespaces...)
domains = append(domains, ghm.Domains.Copilot...)
domains = append(domains, ghm.Domains.Packages...)
slices.Sort(domains)
domains = slices.Compact(domains)

var bareDomains []string
for _, domain := range domains {
trimmed := strings.TrimPrefix(domain, "*.")
if trimmed != domain {
bareDomains = append(bareDomains, trimmed)
}
}
domains = append(domains, bareDomains...)
slices.Sort(domains)
domains = slices.Compact(domains)

fmt.Println(`"domains": [`)
for _, domain := range domains {
fmt.Printf(`"%s",%s`, domain, "\n")
}
fmt.Println(`]`)

advertiseRoutes(set)
}

0 comments on commit 3a635db

Please sign in to comment.