From 3a635db06e61b2614f748b5d9fb90c770456d98b Mon Sep 17 00:00:00 2001 From: James Tucker Date: Thu, 14 Dec 2023 17:20:32 -0800 Subject: [PATCH] cmd/connector-gen: add helper tool for wide app connector configurations 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 --- cmd/connector-gen/README.md | 15 ++++ cmd/connector-gen/advertise-routes.go | 22 +++++ cmd/connector-gen/aws.go | 68 +++++++++++++++ cmd/connector-gen/connector-gen.go | 34 ++++++++ cmd/connector-gen/github.go | 116 ++++++++++++++++++++++++++ 5 files changed, 255 insertions(+) create mode 100644 cmd/connector-gen/README.md create mode 100644 cmd/connector-gen/advertise-routes.go create mode 100644 cmd/connector-gen/aws.go create mode 100644 cmd/connector-gen/connector-gen.go create mode 100644 cmd/connector-gen/github.go diff --git a/cmd/connector-gen/README.md b/cmd/connector-gen/README.md new file mode 100644 index 000000000..071c3ee1f --- /dev/null +++ b/cmd/connector-gen/README.md @@ -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. + + diff --git a/cmd/connector-gen/advertise-routes.go b/cmd/connector-gen/advertise-routes.go new file mode 100644 index 000000000..446f4906a --- /dev/null +++ b/cmd/connector-gen/advertise-routes.go @@ -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() +} diff --git a/cmd/connector-gen/aws.go b/cmd/connector-gen/aws.go new file mode 100644 index 000000000..bd2632ae2 --- /dev/null +++ b/cmd/connector-gen/aws.go @@ -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) +} diff --git a/cmd/connector-gen/connector-gen.go b/cmd/connector-gen/connector-gen.go new file mode 100644 index 000000000..6947f6410 --- /dev/null +++ b/cmd/connector-gen/connector-gen.go @@ -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) + } +} diff --git a/cmd/connector-gen/github.go b/cmd/connector-gen/github.go new file mode 100644 index 000000000..def40872d --- /dev/null +++ b/cmd/connector-gen/github.go @@ -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) +}