Add pre-commit config Update default consensus with more sources Move IP version spec to Source so that dual-stack sites can be used Add tests for HTTPSource and consensus
187 lines
5.7 KiB
Go
187 lines
5.7 KiB
Go
package externalip
|
|
|
|
import (
|
|
"log"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const IPv6 = 6
|
|
const IPv4 = 4
|
|
|
|
// DefaultConsensusConfig returns the ConsensusConfig,
|
|
// with the default values:
|
|
// + Timeout: 30 seconds;
|
|
func DefaultConsensusConfig() *ConsensusConfig {
|
|
return &ConsensusConfig{
|
|
Timeout: time.Second * 30,
|
|
}
|
|
}
|
|
|
|
// DefaultConsensus returns a consensus filled
|
|
// with default and recommended HTTPSources.
|
|
// TLS-Protected providers get more power,
|
|
// compared to plain-text providers.
|
|
func DefaultConsensus(cfg *ConsensusConfig, logger *log.Logger) *Consensus {
|
|
consensus := NewConsensus(cfg, logger)
|
|
|
|
// TLS-protected providers
|
|
_ = consensus.AddVoter(NewHTTPSource("https://myip-mnpz5ateaq-uw.a.run.app", IPv4), 3)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://myip-mnpz5ateaq-uw.a.run.app", IPv6), 3)
|
|
|
|
_ = consensus.AddVoter(NewHTTPSource("https://api.my-ip.io/ip", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://api.my-ip.io/ip", IPv6), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://wtfismyip.com/text", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://wtfismyip.com/text", IPv6), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://icanhazip.com", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://icanhazip.com", IPv6), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://api.ip.sb/ip", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://api.ip.sb/ip", IPv6), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://api.myip.la", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://api.myip.la", IPv6), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://ident.me", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://ident.me", IPv6), 1)
|
|
|
|
_ = consensus.AddVoter(NewHTTPSource("https://api.ipify.org/?format=plain", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("http://api.ipaddress.com/myip", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://ipecho.net/plain", IPv4), 1)
|
|
|
|
// Plain-text providers
|
|
_ = consensus.AddVoter(NewHTTPSource("https://ifconfig.io/ip", IPv6), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://ifconfig.io/ip", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://checkip.amazonaws.com/", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("http://whatismyip.akamai.com/", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://tnx.nl/", IPv6), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://tnx.nl/", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("http://myip.dnsomatic.com/", IPv4), 1)
|
|
_ = consensus.AddVoter(NewHTTPSource("https://diagnostic.opendns.com/myip", IPv6), 1)
|
|
|
|
return consensus
|
|
}
|
|
|
|
// NewConsensus creates a new Consensus, with no sources.
|
|
// When the given cfg is <nil>, the `DefaultConsensusConfig` will be used.
|
|
func NewConsensus(cfg *ConsensusConfig, logger *log.Logger) *Consensus {
|
|
if cfg == nil {
|
|
cfg = DefaultConsensusConfig()
|
|
}
|
|
if logger == nil {
|
|
logger = NewLogger(nil)
|
|
}
|
|
return &Consensus{
|
|
timeout: cfg.Timeout,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// ConsensusConfig is used to configure the Consensus, while creating it.
|
|
type ConsensusConfig struct {
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// WithTimeout sets the voting timeout of this config,
|
|
// returning the config itself at the end, to allow for chaining
|
|
func (cfg *ConsensusConfig) WithTimeout(timeout time.Duration) *ConsensusConfig {
|
|
cfg.Timeout = timeout
|
|
return cfg
|
|
}
|
|
|
|
// Consensus the type at the center of this library,
|
|
// and is the main entry point for users.
|
|
// Its `ExternalIP` method allows you to ask for your ExternalIP,
|
|
// influenced by all its added voters.
|
|
type Consensus struct {
|
|
voters4 []voter
|
|
voters6 []voter
|
|
timeout time.Duration
|
|
logger *log.Logger
|
|
}
|
|
|
|
// AddVoter adds a voter to this consensus.
|
|
// The source cannot be <nil> and
|
|
// the weight has to be of a value of 1 or above.
|
|
func (c *Consensus) AddVoter(source Source, weight uint) error {
|
|
if source == nil {
|
|
c.logger.Println("[ERROR] could not add voter: no source given")
|
|
return ErrNoSource
|
|
}
|
|
if weight == 0 {
|
|
c.logger.Println("[ERROR] could not add voter: weight cannot be 0")
|
|
return ErrInsufficientWeight
|
|
}
|
|
if source.IPVersion() == 4 {
|
|
c.voters4 = append(c.voters4, voter{
|
|
source: source,
|
|
weight: weight,
|
|
})
|
|
}
|
|
if source.IPVersion() == 6 {
|
|
c.voters6 = append(c.voters6, voter{
|
|
source: source,
|
|
weight: weight,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ExternalIP requests asynchronously the externalIP from all added voters,
|
|
// returning the IP which received the most votes.
|
|
// The returned IP will always be valid, in case the returned error is <nil>.
|
|
func (c *Consensus) ExternalIP(ipversion uint) (net.IP, error) {
|
|
voteCollection := make(map[string]uint)
|
|
var vlock sync.Mutex
|
|
var wg sync.WaitGroup
|
|
var thisVoters []voter
|
|
if ipversion == IPv4 {
|
|
thisVoters = c.voters4
|
|
}
|
|
if ipversion == IPv6 {
|
|
thisVoters = c.voters6
|
|
}
|
|
// start all source Requests on a seperate goroutine
|
|
for _, v := range thisVoters {
|
|
wg.Add(1)
|
|
go func(v voter) {
|
|
defer wg.Done()
|
|
ip, err := v.source.IP(c.timeout, c.logger)
|
|
if err == nil && ip != nil {
|
|
vlock.Lock()
|
|
defer vlock.Unlock()
|
|
voteCollection[ip.String()] += v.weight
|
|
}
|
|
}(v)
|
|
}
|
|
|
|
// wait for all votes to come in,
|
|
// or until their process times out
|
|
wg.Wait()
|
|
|
|
// if no votes were casted succesfully,
|
|
// return early with an error
|
|
if len(voteCollection) == 0 {
|
|
c.logger.Println("[ERROR] no votes were casted succesfully")
|
|
return nil, ErrNoIP
|
|
}
|
|
|
|
var max uint
|
|
var externalIP string
|
|
|
|
// find the IP which has received the most votes,
|
|
// influinced by the voter's weight.
|
|
vlock.Lock()
|
|
defer vlock.Unlock()
|
|
for ip, votes := range voteCollection {
|
|
if votes > max {
|
|
max, externalIP = votes, ip
|
|
}
|
|
}
|
|
|
|
// as the found IP was parsed previously,
|
|
// we know it cannot be nil and is valid
|
|
if ipversion == IPv4 {
|
|
return net.ParseIP(externalIP).To4(), nil
|
|
}
|
|
return net.ParseIP(externalIP).To16(), nil
|
|
}
|