Files
gluetun/internal/configuration/settings/publicip.go
Quentin McGaw a61302f135 feat(publicip): resilient public ip fetcher (#2518)
- `PUBLICIP_API` accepts a comma separated list of ip data sources, where the first one is the base default one, and sources after it are backup sources used if we are rate limited.
- `PUBLICIP_API` defaults to `ipinfo,ifconfigco,ip2location,cloudflare` such that it now has `ifconfigco,ip2location,cloudflare` as backup ip data sources.
- `PUBLICIP_API_TOKEN` accepts a comma separated list of ip data source tokens, each corresponding by position to the APIs listed in `PUBLICIP_API`.
- logs ip data source when logging public ip information
- assume a rate limiting error is for 30 days (no persistence)
- ready for future live settings updates
  - consider an ip data source no longer banned if the token changes
  - keeps track of ban times when updating the list of fetchers
2024-10-19 15:21:14 +02:00

176 lines
5.0 KiB
Go

package settings
import (
"fmt"
"path/filepath"
"github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// PublicIP contains settings for port forwarding.
type PublicIP struct {
// Enabled is set to true to fetch the public ip address
// information on VPN connection. It defaults to true.
Enabled *bool
// IPFilepath is the public IP address status file path
// to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the
// internal state
IPFilepath *string
// APIs is the list of public ip APIs to use to fetch public IP information.
// If there is more than one API, the first one is used
// by default and the others are used as fallbacks in case of
// the service rate limiting us. It defaults to use all services,
// with the first one being ipinfo.io for historical reasons.
APIs []PublicIPAPI
}
type PublicIPAPI struct {
// Name is the name of the public ip API service.
// It can be "cloudflare", "ifconfigco", "ip2location" or "ipinfo".
Name string
// Token is the token to use for the public ip API service.
Token string
}
// UpdateWith deep copies the receiving settings, overrides the copy with
// fields set in the partialUpdate argument, validates the new settings
// and returns them if they are valid, or returns an error otherwise.
// In all cases, the receiving settings are unmodified.
func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP, err error) {
updatedSettings = p.copy()
updatedSettings.overrideWith(partialUpdate)
err = updatedSettings.validate()
if err != nil {
return updatedSettings, fmt.Errorf("validating updated settings: %w", err)
}
return updatedSettings, nil
}
func (p PublicIP) validate() (err error) {
if *p.IPFilepath != "" { // optional
_, err := filepath.Abs(*p.IPFilepath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
}
}
for _, publicIPAPI := range p.APIs {
_, err = api.ParseProvider(publicIPAPI.Name)
if err != nil {
return fmt.Errorf("API name: %w", err)
}
}
return nil
}
func (p *PublicIP) copy() (copied PublicIP) {
return PublicIP{
Enabled: gosettings.CopyPointer(p.Enabled),
IPFilepath: gosettings.CopyPointer(p.IPFilepath),
APIs: gosettings.CopySlice(p.APIs),
}
}
func (p *PublicIP) overrideWith(other PublicIP) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
}
func (p *PublicIP) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
{Name: string(api.IPInfo)},
{Name: string(api.Cloudflare)},
{Name: string(api.IfConfigCo)},
{Name: string(api.IP2Location)},
})
}
func (p PublicIP) String() string {
return p.toLinesNode().String()
}
func (p PublicIP) toLinesNode() (node *gotree.Node) {
if !*p.Enabled {
return gotree.New("Public IP settings: disabled")
}
node = gotree.New("Public IP settings:")
if *p.IPFilepath != "" {
node.Appendf("IP file path: %s", *p.IPFilepath)
}
baseAPIString := "Public IP data base API: " + p.APIs[0].Name
if p.APIs[0].Token != "" {
baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
}
node.Append(baseAPIString)
if len(p.APIs) > 1 {
backupAPIsNode := node.Append("Public IP data backup APIs:")
for i := 1; i < len(p.APIs); i++ {
message := p.APIs[i].Name
if p.APIs[i].Token != "" {
message += " (token " + gosettings.ObfuscateKey(p.APIs[i].Token) + ")"
}
backupAPIsNode.Append(message)
}
}
return node
}
func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
p.Enabled, err = readPublicIPEnabled(r, warner)
if err != nil {
return err
}
p.IPFilepath = r.Get("PUBLICIP_FILE",
reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))
apiNames := r.CSV("PUBLICIP_API")
if len(apiNames) > 0 {
apiTokens := r.CSV("PUBLICIP_API_TOKEN")
p.APIs = make([]PublicIPAPI, len(apiNames))
for i := range apiNames {
p.APIs[i].Name = apiNames[i]
var token string
if i < len(apiTokens) { // only set token if it exists
token = apiTokens[i]
}
p.APIs[i].Token = token
}
}
return nil
}
func readPublicIPEnabled(r *reader.Reader, warner Warner) (
enabled *bool, err error,
) {
periodPtr, err := r.DurationPtr("PUBLICIP_PERIOD") // Retro-compatibility
if err != nil {
return nil, err
} else if periodPtr == nil {
return r.BoolPtr("PUBLICIP_ENABLED")
}
if *periodPtr == 0 {
warner.Warn("please replace PUBLICIP_PERIOD=0 with PUBLICIP_ENABLED=no")
return ptrTo(false), nil
}
warner.Warn("PUBLICIP_PERIOD is no longer used. " +
"It is assumed from its non-zero value you want PUBLICIP_ENABLED=yes. " +
"Please migrate to use PUBLICIP_ENABLED only in the future.")
return ptrTo(true), nil
}