Compare commits

..

1 Commits

Author SHA1 Message Date
Timmy Welch
9363450cdc v0.0.1
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
2022-07-07 18:40:35 -07:00
12 changed files with 261 additions and 72 deletions

1
.gitignore vendored
View File

@ -14,4 +14,3 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

18
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,18 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=.gitignore]
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-beta.5
hooks:
- id: go-mod-tidy
- id: go-imports
args: [-w]
- repo: https://github.com/golangci/golangci-lint
rev: v1.46.2
hooks:
- id: golangci-lint

View File

@ -34,7 +34,7 @@ func main() {
consensus := externalip.DefaultConsensus(cfg, logger)
// retrieve the external ip
ip4, err := consensus.ExternalIP(4)
ip4, _ := consensus.ExternalIP(4)
ip6, err := consensus.ExternalIP(6)
// simple error handling

View File

@ -27,17 +27,35 @@ func DefaultConsensus(cfg *ConsensusConfig, logger *log.Logger) *Consensus {
consensus := NewConsensus(cfg, logger)
// TLS-protected providers
consensus.AddVoter(NewHTTPSource("https://icanhazip.com/"), 3, IPv6)
consensus.AddVoter(NewHTTPSource("https://myexternalip.com/raw"), 3, IPv4)
_ = 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("http://ifconfig.io/ip"), 1, IPv6)
consensus.AddVoter(NewHTTPSource("http://checkip.amazonaws.com/"), 1, IPv4)
consensus.AddVoter(NewHTTPSource("http://ident.me/"), 1, IPv6)
consensus.AddVoter(NewHTTPSource("http://whatismyip.akamai.com/"), 1, IPv4)
consensus.AddVoter(NewHTTPSource("http://tnx.nl/ip"), 1, IPv6)
consensus.AddVoter(NewHTTPSource("http://myip.dnsomatic.com/"), 1, IPv4)
consensus.AddVoter(NewHTTPSource("http://diagnostic.opendns.com/myip"), 1, IPv6)
_ = 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
}
@ -83,7 +101,7 @@ type Consensus struct {
// 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, ipversion uint) error {
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
@ -92,13 +110,13 @@ func (c *Consensus) AddVoter(source Source, weight, ipversion uint) error {
c.logger.Println("[ERROR] could not add voter: weight cannot be 0")
return ErrInsufficientWeight
}
if ipversion == 4 {
if source.IPVersion() == 4 {
c.voters4 = append(c.voters4, voter{
source: source,
weight: weight,
})
}
if ipversion == 6 {
if source.IPVersion() == 6 {
c.voters6 = append(c.voters6, voter{
source: source,
weight: weight,

View File

@ -1,30 +1,91 @@
package externalip
package externalip_test
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.narnian.us/lordwelch/externalip"
)
func TestDefaultConsensus(t *testing.T) {
consensus := DefaultConsensus(nil)
func newConcensus(url string) *externalip.Consensus {
consensus := externalip.NewConsensus(&externalip.ConsensusConfig{
Timeout: time.Second * 30,
}, nil)
// TLS-protected providers
_ = consensus.AddVoter(externalip.NewHTTPSource(url, externalip.IPv4), 1)
_ = consensus.AddVoter(externalip.NewHTTPSource(url, externalip.IPv4), 1)
_ = consensus.AddVoter(externalip.NewHTTPSource(url, externalip.IPv4), 1)
return consensus
}
func newServer(ip1, ip2, ip3 string) *httptest.Server {
var try = 1
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
switch try {
case 1:
fmt.Fprintln(w, ip1)
case 2:
fmt.Fprintln(w, ip2)
case 3:
fmt.Fprintln(w, ip3)
try = 1
}
try++
}))
}
func TestConsensus(t *testing.T) {
server := newServer("127.0.0.1", "127.0.0.1", "127.0.0.2")
consensus := newConcensus(server.URL)
if consensus == nil {
t.Fatal("default consensus should never be nil")
}
ip, err := consensus.ExternalIP()
ip, err := consensus.ExternalIP(4)
if err != nil {
t.Fatal("couldn't get external IP", err)
}
fmt.Println(ip)
for i := 0; i < 2; i++ {
ipAgain, err := consensus.ExternalIP()
if err != nil {
t.Fatal("couldn't get external IP", err)
}
if !ip.Equal(ipAgain) {
t.Fatalf("expected %q, while received %q", ip, ipAgain)
}
if !ip.Equal(net.IPv4(127, 0, 0, 1)) {
t.Errorf("invalid ip found: expected %s recieved %s", "127.0.0.1", ip)
}
}
func TestConsensus2(t *testing.T) {
server := newServer("127.0.0.1", "127.0.0.2", "127.0.0.2")
consensus := newConcensus(server.URL)
if consensus == nil {
t.Fatal("default consensus should never be nil")
}
ip, err := consensus.ExternalIP(4)
if err != nil {
t.Fatal("couldn't get external IP", err)
}
if !ip.Equal(net.IPv4(127, 0, 0, 2)) {
t.Errorf("invalid ip found: expected %s recieved %s", "127.0.0.2", ip)
}
}
func TestConsensus3(t *testing.T) {
server := newServer("127.0.0.1", "127.0.0.2", "127.0.0.3")
consensus := newConcensus(server.URL)
if consensus == nil {
t.Fatal("default consensus should never be nil")
}
ip, err := consensus.ExternalIP(4)
if err != nil {
t.Fatal("couldn't get external IP", err)
}
if !ip.Equal(net.IPv4(127, 0, 0, 1)) {
t.Errorf("invalid ip found: expected %s recieved %s; This will fail on multiple runs it takes whichever request returns first", "127.0.0.1", ip)
}
}

11
go.mod
View File

@ -1,12 +1,3 @@
module git.narnian.us/lordwelch/externalip
go 1.12
require (
github.com/google/pprof v0.0.0-20190908185732-236ed259b199 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect
golang.org/x/arch v0.0.0-20190919213554-7fe50f7625bd // indirect
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 // indirect
golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect
golang.org/x/tools v0.0.0-20190919223014-db1d4edb4685 // indirect
)
go 1.18

21
go.sum
View File

@ -1,21 +0,0 @@
github.com/google/pprof v0.0.0-20190908185732-236ed259b199 h1:sEyCq3pOT7tNC+3gcLI7sZkBDgntZ6wQJNmr9lmIjIc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
golang.org/x/arch v0.0.0-20190919213554-7fe50f7625bd h1:IbZRdF+nCjC31g8APRj0sBwNe35VSPHNThzJKA0idTs=
golang.org/x/arch v0.0.0-20190919213554-7fe50f7625bd/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k=
golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190919223014-db1d4edb4685 h1:/6Ol4IqB+r3aIk191dJQFcnPHMW+pj8RzXAz3ddkmk4=
golang.org/x/tools v0.0.0-20190919223014-db1d4edb4685/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,6 +1,7 @@
package externalip
import (
"context"
"io/ioutil"
"log"
"net"
@ -12,9 +13,10 @@ import (
// NewHTTPSource creates a HTTP Source object,
// which can be used to request the (external) IP from.
// The Default HTTP Client will be used if no client is given.
func NewHTTPSource(url string) *HTTPSource {
func NewHTTPSource(url string, ipversion uint) *HTTPSource {
return &HTTPSource{
url: url,
ipversion: ipversion,
}
}
@ -23,6 +25,7 @@ func NewHTTPSource(url string) *HTTPSource {
type HTTPSource struct {
url string
parser ContentParser
ipversion uint
}
// ContentParser can be used to add a parser to an HTTPSource
@ -39,27 +42,57 @@ func (s *HTTPSource) WithParser(parser ContentParser) *HTTPSource {
// IP implements Source.IP
func (s *HTTPSource) IP(timeout time.Duration, logger *log.Logger) (net.IP, error) {
var (
dialer = &net.Dialer{
Timeout: 30 * time.Second,
DualStack: false,
FallbackDelay: -1,
KeepAlive: 30 * time.Second,
}
n = "tcp4"
)
if s.ipversion == 6 {
n = "tcp6"
}
// Define the GET method with the correct url,
// setting the User-Agent to our library
req, err := http.NewRequest("GET", s.url, nil)
if err != nil {
if logger != nil {
logger.Printf("[ERROR] could not create a GET Request for %q: %v\n", s.url, err)
}
return nil, err
}
req.Header.Set("User-Agent", "go-external-ip (github.com/glendc/go-external-ip)")
client := &http.Client{Timeout: timeout}
client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, n, addr)
},
TLSHandshakeTimeout: 10 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
ResponseHeaderTimeout: timeout,
ExpectContinueTimeout: 1 * time.Second,
},
Timeout: timeout,
}
// Do the request and read the body for non-error results.
resp, err := client.Do(req)
if err != nil {
if logger != nil {
logger.Printf("[ERROR] could not GET %q: %v\n", s.url, err)
}
return nil, err
}
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
if logger != nil {
logger.Printf("[ERROR] could not read response from %q: %v\n", s.url, err)
}
return nil, err
}
@ -68,7 +101,9 @@ func (s *HTTPSource) IP(timeout time.Duration, logger *log.Logger) (net.IP, erro
if s.parser != nil {
raw, err = s.parser(raw)
if err != nil {
if logger != nil {
logger.Printf("[ERROR] could not parse response from %q: %v\n", s.url, err)
}
return nil, err
}
}
@ -76,10 +111,16 @@ func (s *HTTPSource) IP(timeout time.Duration, logger *log.Logger) (net.IP, erro
// validate the IP
externalIP := net.ParseIP(strings.TrimSpace(raw))
if externalIP == nil {
if logger != nil {
logger.Printf("[ERROR] %q returned an invalid IP: %v\n", s.url, err)
}
return nil, InvalidIPError(raw)
}
// returned the parsed IP
return externalIP, nil
}
func (s *HTTPSource) IPVersion() uint {
return s.ipversion
}

81
sources_test.go Normal file
View File

@ -0,0 +1,81 @@
package externalip_test
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.narnian.us/lordwelch/externalip"
)
func TestHTTPSource(t *testing.T) {
server := httptest.NewUnstartedServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, r.RemoteAddr[1:len(r.RemoteAddr)-7])
}))
server.Listener.Close()
server.Listener, _ = net.Listen("tcp6", "[::1]:0")
server.Start()
defer server.Close()
source := externalip.NewHTTPSource("http://"+server.Listener.Addr().String(), 6)
ip, _ := source.IP(time.Second*10, nil)
if ip.String() != "::1" {
t.Errorf("invalid ip found: expected %s recieved %s", "::1", ip)
}
}
func TestHTTPSource2(t *testing.T) {
server := httptest.NewUnstartedServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, r.RemoteAddr[1:len(r.RemoteAddr)-7])
}))
server.Listener.Close()
server.Listener, _ = net.Listen("tcp6", "[::1]:0")
server.Start()
defer server.Close()
source := externalip.NewHTTPSource("http://"+server.Listener.Addr().String(), 4)
ip, _ := source.IP(time.Second*10, nil)
if ip != nil {
t.Errorf("invalid ip found: expected %s recieved %s", "nil", ip)
}
}
func TestHTTPSource3(t *testing.T) {
server := httptest.NewUnstartedServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, r.RemoteAddr[:len(r.RemoteAddr)-6])
}))
server.Listener.Close()
server.Listener, _ = net.Listen("tcp4", "127.0.0.1:0")
server.Start()
defer server.Close()
source := externalip.NewHTTPSource("http://"+server.Listener.Addr().String(), 4)
ip, _ := source.IP(time.Second*10, nil)
if ip.String() != "127.0.0.1" {
t.Errorf("invalid ip found: expected %s recieved %s", "127.0.0.1", ip)
}
}
func TestHTTPSource4(t *testing.T) {
server := httptest.NewUnstartedServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, r.RemoteAddr[:len(r.RemoteAddr)-6])
}))
server.Listener.Close()
server.Listener, _ = net.Listen("tcp4", "127.0.0.1:0")
server.Start()
defer server.Close()
source := externalip.NewHTTPSource("http://"+server.Listener.Addr().String(), 6)
ip, _ := source.IP(time.Second*10, nil)
if ip != nil {
t.Errorf("invalid ip found: expected %s recieved %s", "nil", ip)
}
}

View File

@ -13,6 +13,7 @@ type Source interface {
// It is recommended that the IP function times out,
// if no result could be found, after the given timeout duration.
IP(timeout time.Duration, logger *log.Logger) (net.IP, error)
IPVersion() uint
}
// voter adds weight to the IP given by a source.