diff --git a/Dockerfile b/Dockerfile index 0e9eefee..dc5eac45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -190,6 +190,7 @@ ENV VPN_SERVICE_PROVIDER=pia \ # Public IP PUBLICIP_FILE="/tmp/gluetun/ip" \ PUBLICIP_PERIOD=12h \ + PUBLICIP_API_TOKEN= \ # Pprof PPROF_ENABLED=no \ PPROF_BLOCK_PROFILE_RATE=0 \ diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index d84b8305..2a8968d1 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -396,7 +396,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone) controlGroupHandler.Add(dnsTickerHandler) - ipFetcher := ipinfo.New(httpClient) + ipFetcher := ipinfo.New(httpClient, *allSettings.PublicIP.APIToken) publicIPLooper := publicip.NewLoop(ipFetcher, logger.New(log.SetComponent("ip getter")), allSettings.PublicIP, puid, pgid) diff --git a/internal/cli/update.go b/internal/cli/update.go index 07e5899b..1412ed2b 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -35,7 +35,7 @@ type UpdaterLogger interface { func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error { options := settings.Updater{} var endUserMode, maintainerMode, updateAll bool - var csvProviders string + var csvProviders, ipToken string flagSet := flag.NewFlagSet("update", flag.ExitOnError) flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)") flagSet.BoolVar(&maintainerMode, "maintainer", false, @@ -46,6 +46,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e "Minimum ratio of servers to find for the update to succeed") flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers") flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for") + flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use") if err := flagSet.Parse(args); err != nil { return err } @@ -79,7 +80,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e httpClient := &http.Client{Timeout: clientTimeout} unzipper := unzip.New(httpClient) parallelResolver := resolver.NewParallelResolver(options.DNSAddress) - ipFetcher := ipinfo.New(httpClient) + ipFetcher := ipinfo.New(httpClient, ipToken) openvpnFileExtractor := extract.New() providers := provider.NewProviders(storage, time.Now, logger, httpClient, diff --git a/internal/configuration/settings/publicip.go b/internal/configuration/settings/publicip.go index c7b11a69..3e34bc3e 100644 --- a/internal/configuration/settings/publicip.go +++ b/internal/configuration/settings/publicip.go @@ -21,6 +21,11 @@ type PublicIP struct { // to write to a file. It cannot be nil for the // internal state IPFilepath *string + // APIToken is the token to use for the IP data service + // such as ipinfo.io. It can be the empty string to + // indicate not to use a token. It cannot be nil for the + // internal state. + APIToken *string } // UpdateWith deep copies the receiving settings, overrides the copy with @@ -58,23 +63,27 @@ func (p *PublicIP) copy() (copied PublicIP) { return PublicIP{ Period: gosettings.CopyPointer(p.Period), IPFilepath: gosettings.CopyPointer(p.IPFilepath), + APIToken: gosettings.CopyPointer(p.APIToken), } } func (p *PublicIP) mergeWith(other PublicIP) { p.Period = gosettings.MergeWithPointer(p.Period, other.Period) p.IPFilepath = gosettings.MergeWithPointer(p.IPFilepath, other.IPFilepath) + p.APIToken = gosettings.MergeWithPointer(p.APIToken, other.APIToken) } func (p *PublicIP) overrideWith(other PublicIP) { p.Period = gosettings.OverrideWithPointer(p.Period, other.Period) p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath) + p.APIToken = gosettings.OverrideWithPointer(p.APIToken, other.APIToken) } func (p *PublicIP) setDefaults() { const defaultPeriod = 12 * time.Hour p.Period = gosettings.DefaultPointer(p.Period, defaultPeriod) p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip") + p.APIToken = gosettings.DefaultPointer(p.APIToken, "") } func (p PublicIP) String() string { @@ -99,5 +108,9 @@ func (p PublicIP) toLinesNode() (node *gotree.Node) { node.Appendf("IP file path: %s", *p.IPFilepath) } + if *p.APIToken != "" { + node.Appendf("API token: %s", gosettings.ObfuscateKey(*p.APIToken)) + } + return node } diff --git a/internal/configuration/sources/env/publicip.go b/internal/configuration/sources/env/publicip.go index 145e8141..50bec915 100644 --- a/internal/configuration/sources/env/publicip.go +++ b/internal/configuration/sources/env/publicip.go @@ -14,5 +14,7 @@ func (s *Source) readPublicIP() (publicIP settings.PublicIP, err error) { publicIP.IPFilepath = s.env.Get("PUBLICIP_FILE", env.ForceLowercase(false), env.RetroKeys("IP_STATUS_FILE")) + publicIP.APIToken = s.env.Get("PUBLICIP_API_TOKEN") + return publicIP, nil } diff --git a/internal/publicip/ipinfo/fetch.go b/internal/publicip/ipinfo/fetch.go index 4190a325..6f625c52 100644 --- a/internal/publicip/ipinfo/fetch.go +++ b/internal/publicip/ipinfo/fetch.go @@ -14,15 +14,18 @@ import ( type Fetch struct { client *http.Client + token string } -func New(client *http.Client) *Fetch { +func New(client *http.Client, token string) *Fetch { return &Fetch{ client: client, + token: token, } } var ( + ErrTokenNotValid = errors.New("token is not valid") ErrTooManyRequests = errors.New("too many requests sent for this month") ErrBadHTTPStatus = errors.New("bad HTTP status received") ) @@ -44,6 +47,7 @@ func (f *Fetch) FetchInfo(ctx context.Context, ip netip.Addr) ( if err != nil { return result, err } + request.Header.Set("Authorization", "Bearer "+f.token) response, err := f.client.Do(request) if err != nil { @@ -51,6 +55,10 @@ func (f *Fetch) FetchInfo(ctx context.Context, ip netip.Addr) ( } defer response.Body.Close() + if f.token != "" && response.StatusCode == http.StatusUnauthorized { + return result, fmt.Errorf("%w: %s", ErrTokenNotValid, response.Status) + } + switch response.StatusCode { case http.StatusOK: case http.StatusTooManyRequests, http.StatusForbidden: