From b178c46be71c20246b1145b7c5e6f7e205d80872 Mon Sep 17 00:00:00 2001 From: Fran Bull Date: Thu, 2 May 2024 12:27:59 -0700 Subject: [PATCH] cmd/connector-gen: reduce the routes for github This script produces output that contains thousands of routes. That's more than really works well for a lot of tailnets, so reduce the number of routes, while still enabling use of GitHub's enterprise "allowed IPs" feature for the app connector IP. Updates tailscale/corp/#19424 Signed-off-by: Fran Bull --- cmd/connector-gen/github.go | 146 ++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 57 deletions(-) diff --git a/cmd/connector-gen/github.go b/cmd/connector-gen/github.go index def40872d..83d7d2604 100644 --- a/cmd/connector-gen/github.go +++ b/cmd/connector-gen/github.go @@ -13,104 +13,136 @@ import ( "strings" "go4.org/netipx" + xmaps "golang.org/x/exp/maps" ) +// omitDomains are domains that appear in the github API /meta output +// that we do not need to have app connectors route traffic for (and +// to do so would result in advertising more routes than we want). +var omitDomains = map[string]bool{ + "*.githubassets.com": true, + "*.githubusercontent.com": true, + "*.windows.net": true, + "*.azureedge.net": true, +} + // See https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-githubs-ip-addresses +// GithubMeta is a subset of the response from the github APIs /meta endpoint. 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) +func (ghm GithubMeta) routesLists() [][]string { + return [][]string{ + ghm.Web, + ghm.API, + ghm.Git, + ghm.GithubEnterpriseImporter, + ghm.Packages, + ghm.Pages, } +} - var ghm GithubMeta - - if err := json.NewDecoder(r.Body).Decode(&ghm); err != nil { - log.Fatal(err) +func (ghm GithubMeta) domainsLists() [][]string { + return [][]string{ + ghm.Domains.Website, + ghm.Domains.Codespaces, + ghm.Domains.Copilot, } - r.Body.Close() +} +func (ghm GithubMeta) routes() *netipx.IPSet { var ips netipx.IPSetBuilder + for _, routes := range ghm.routesLists() { + for _, r := range routes { + ips.AddPrefix(netip.MustParsePrefix(r)) + } + } + set, err := ips.IPSet() + if err != nil { + log.Fatal(err) + } + return set +} - 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)) +func (ghm GithubMeta) domains() []string { + ds := map[string]bool{} + for _, list := range ghm.domainsLists() { + for _, d := range list { + if !omitDomains[d] { + ds[d] = true + } + } } + return xmaps.Keys(ds) +} - set, err := ips.IPSet() +type Output struct { + Routes []netip.Prefix `json:"routes"` + Domains []string `json:"domains"` +} + +func (o Output) format() []byte { + s, err := json.MarshalIndent(o, "", " ") if err != nil { log.Fatal(err) } + return s +} - fmt.Println(`"routes": [`) - for _, pfx := range set.Prefixes() { - fmt.Printf(`"%s": ["tag:connector"],%s`, pfx.String(), "\n") +// github prints app connector config to standard out. +// The /meta github endpoint lists the routes and domains needed to use GitHub. It +// lists thousands of routes, and includes broad wildcard domains like *.microsoft.com. +// Not all tailnets function well with an app connector that's advertising thousands of +// routes. +// GitHub has an enterprise "allowed IPs only" feature. The goal of this script is +// to capture only the domains and routes needed to configure an app connector so that +// users of that app connector can enable that GitHub feature pointing at the app connector +// IP address and have github work. +// We don't know exactly which routes and domains are needed, but I got an email from GitHub +// support saying that only the routes provided in 'web', 'api', and 'git' are needed, +// but that doesn't seem very likely, surely users of eg private packages will +// need to be coming from an allowed IP? Still, attempt to be reasonably restrictive. +func github() { + r, err := http.Get("https://api.github.com/meta") + if err != nil { + log.Fatal(err) } - fmt.Println(`]`) - fmt.Println() + var ghm GithubMeta + if err := json.NewDecoder(r.Body).Decode(&ghm); err != nil { + log.Fatal(err) + } + r.Body.Close() 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 { + for _, domain := range ghm.domains() { + domains = append(domains, domain) trimmed := strings.TrimPrefix(domain, "*.") if trimmed != domain { - bareDomains = append(bareDomains, trimmed) + domains = append(domains, 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(`]`) + set := ghm.routes() + + fmt.Println(string(Output{ + Routes: set.Prefixes(), + Domains: domains, + }.format())) advertiseRoutes(set) }