Compare commits

..

No commits in common. "master" and "v1.0.0" have entirely different histories.

12 changed files with 72 additions and 261 deletions

@ -6,6 +6,6 @@ Please remove the sections that don't apply
## Environment
go-external-ip commit:
go-external-ip commit:
go version:
OS:

1
.gitignore vendored

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

@ -1,18 +0,0 @@
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

@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
THE SOFTWARE.

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

@ -27,35 +27,17 @@ 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)
consensus.AddVoter(NewHTTPSource("https://icanhazip.com/"), 3, IPv6)
consensus.AddVoter(NewHTTPSource("https://myexternalip.com/raw"), 3, IPv4)
// 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)
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)
return consensus
}
@ -101,7 +83,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 uint) error {
func (c *Consensus) AddVoter(source Source, weight, ipversion uint) error {
if source == nil {
c.logger.Println("[ERROR] could not add voter: no source given")
return ErrNoSource
@ -110,13 +92,13 @@ func (c *Consensus) AddVoter(source Source, weight uint) error {
c.logger.Println("[ERROR] could not add voter: weight cannot be 0")
return ErrInsufficientWeight
}
if source.IPVersion() == 4 {
if ipversion == 4 {
c.voters4 = append(c.voters4, voter{
source: source,
weight: weight,
})
}
if source.IPVersion() == 6 {
if ipversion == 6 {
c.voters6 = append(c.voters6, voter{
source: source,
weight: weight,

@ -1,91 +1,30 @@
package externalip_test
package externalip
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.narnian.us/lordwelch/externalip"
)
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)
func TestDefaultConsensus(t *testing.T) {
consensus := DefaultConsensus(nil)
if consensus == nil {
t.Fatal("default consensus should never be nil")
}
ip, err := consensus.ExternalIP(4)
ip, err := consensus.ExternalIP()
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", "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)
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)
}
}
}

11
go.mod

@ -1,3 +1,12 @@
module git.narnian.us/lordwelch/externalip
go 1.18
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
)

21
go.sum

@ -0,0 +1,21 @@
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=

@ -1,7 +1,6 @@
package externalip
import (
"context"
"io/ioutil"
"log"
"net"
@ -13,19 +12,17 @@ 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, ipversion uint) *HTTPSource {
func NewHTTPSource(url string) *HTTPSource {
return &HTTPSource{
url: url,
ipversion: ipversion,
url: url,
}
}
// HTTPSource is the default source, to get the external IP from.
// It does so by requesting the IP from a URL, via an HTTP GET Request.
type HTTPSource struct {
url string
parser ContentParser
ipversion uint
url string
parser ContentParser
}
// ContentParser can be used to add a parser to an HTTPSource
@ -42,57 +39,27 @@ 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)
}
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{
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,
}
client := &http.Client{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)
}
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)
}
logger.Printf("[ERROR] could not read response from %q: %v\n", s.url, err)
return nil, err
}
@ -101,9 +68,7 @@ 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)
}
logger.Printf("[ERROR] could not parse response from %q: %v\n", s.url, err)
return nil, err
}
}
@ -111,16 +76,10 @@ 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)
}
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
}

@ -1,81 +0,0 @@
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)
}
}

@ -13,7 +13,6 @@ 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.