mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
06cb1742a1
getPublicIP read a third-party IP echo service's body whole and cached whatever came back as the panel's address, so one misbehaving service could make the panel inflate an endless response at startup, and an error page became the address shown in every status sample. The read is capped at 64 bytes and only a parsable IP is accepted; anything else counts as unavailable. The GitHub release lookups are capped as well.
594 lines
19 KiB
Go
594 lines
19 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
)
|
|
|
|
// A box with no IPv6 route spends 3s per lookup service, and a status sample
|
|
// that waits for that is a panel reporting nothing for the first ~15s.
|
|
func TestStatusSampleDoesNotWaitOnPublicIPLookup(t *testing.T) {
|
|
dbDir := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", dbDir)
|
|
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
|
|
|
release := make(chan struct{})
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte("203.0.113.7"))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
savedV4, savedV6 := publicIPv4Services, publicIPv6Services
|
|
publicIPv4Services = []string{srv.URL}
|
|
publicIPv6Services = []string{srv.URL}
|
|
t.Cleanup(func() { publicIPv4Services, publicIPv6Services = savedV4, savedV6 })
|
|
|
|
svc := &ServerService{}
|
|
status := svc.CurrentStatus()
|
|
if status == nil {
|
|
t.Fatal("CurrentStatus returned nil while the IP lookup was in flight")
|
|
}
|
|
if status.PublicIP.IPv4 != "" {
|
|
t.Fatalf("the sample waited for the lookup: PublicIP.IPv4 = %q, want it still unresolved", status.PublicIP.IPv4)
|
|
}
|
|
|
|
close(release)
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
for {
|
|
if ipv4, _ := svc.publicIPs(); ipv4 == "203.0.113.7" {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("the background lookup never cached the public IP")
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// publicIPTestTransport keeps clock-driven tests entirely off the network.
|
|
type publicIPTestTransport func(*http.Request) (*http.Response, error)
|
|
|
|
func (f publicIPTestTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
|
|
|
func stubPublicIPServices(t *testing.T, respond publicIPTestTransport) {
|
|
t.Helper()
|
|
oldTransport, old4, old6 := http.DefaultTransport, publicIPv4Services, publicIPv6Services
|
|
http.DefaultTransport = respond
|
|
publicIPv4Services = []string{"https://ipv4.example.test/primary", "https://ipv4.example.test/fallback"}
|
|
publicIPv6Services = []string{"https://ipv6.example.test/primary", "https://ipv6.example.test/fallback"}
|
|
t.Cleanup(func() { http.DefaultTransport, publicIPv4Services, publicIPv6Services = oldTransport, old4, old6 })
|
|
}
|
|
|
|
func publicIPResponse(status int, body string) (*http.Response, error) {
|
|
return &http.Response{StatusCode: status, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body))}, nil
|
|
}
|
|
|
|
func assertCachedPublicIPs(t *testing.T, svc *ServerService, want4, want6 string) {
|
|
t.Helper()
|
|
got4, got6 := svc.publicIPs()
|
|
if got4 != want4 || got6 != want6 {
|
|
t.Fatalf("cached IPs=(%q,%q), want (%q,%q)", got4, got6, want4, want6)
|
|
}
|
|
}
|
|
|
|
func TestPublicIPRefreshesAfterExpiryWithoutRestart(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls atomic.Int32
|
|
value4, value6 := "192.0.2.10", "2001:db8::10"
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, value4)
|
|
}
|
|
return publicIPResponse(200, value6)
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
assertCachedPublicIPs(t, svc, value4, value6)
|
|
value4, value6 = "192.0.2.20", "2001:db8::20"
|
|
time.Sleep(5*time.Minute - time.Nanosecond)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
if got := calls.Load(); got != 2 {
|
|
t.Fatalf("lookup before expiry: %d requests", got)
|
|
}
|
|
time.Sleep(time.Nanosecond)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
assertCachedPublicIPs(t, svc, value4, value6)
|
|
if got := calls.Load(); got != 4 {
|
|
t.Fatalf("refresh requests=%d, want 4", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPublicIPFailedRefreshRetainsLastKnownAndRetries(t *testing.T) {
|
|
for _, failure := range []string{"http-error", "empty-body", "transport-error", "timeout"} {
|
|
t.Run(failure, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls atomic.Int32
|
|
unavailable := false
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
if unavailable {
|
|
switch failure {
|
|
case "empty-body":
|
|
return publicIPResponse(200, "")
|
|
case "transport-error":
|
|
return nil, io.ErrUnexpectedEOF
|
|
case "timeout":
|
|
<-r.Context().Done()
|
|
return nil, r.Context().Err()
|
|
default:
|
|
return publicIPResponse(503, "")
|
|
}
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, "192.0.2.10")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::10")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
unavailable = true
|
|
time.Sleep(5 * time.Minute)
|
|
// Await the refresh, including HTTP timeouts, before checking its result.
|
|
done := svc.resolvePublicIPsInBackground()
|
|
if done == nil {
|
|
t.Fatal("expired cache did not start a refresh")
|
|
}
|
|
<-done
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::10")
|
|
if got := calls.Load(); got != 6 {
|
|
t.Fatalf("failed refresh made %d requests, want 6 including fallbacks", got)
|
|
}
|
|
time.Sleep(time.Minute - time.Nanosecond)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
if got := calls.Load(); got != 6 {
|
|
t.Fatalf("retried too early: %d requests", got)
|
|
}
|
|
unavailable = false
|
|
time.Sleep(time.Nanosecond)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
if got := calls.Load(); got != 8 {
|
|
t.Fatalf("retry requests=%d, want 8", got)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublicIPInitialFailureRecoversBothFamilies(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
available := false
|
|
var calls atomic.Int32
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
if !available {
|
|
return publicIPResponse(503, "")
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, "192.0.2.20")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::20")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
assertCachedPublicIPs(t, svc, "N/A", "N/A")
|
|
available = true
|
|
time.Sleep(time.Minute)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
assertCachedPublicIPs(t, svc, "192.0.2.20", "2001:db8::20")
|
|
if got := calls.Load(); got != 6 {
|
|
t.Fatalf("initial failure/recovery requests=%d, want 6", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPublicIPFamiliesRefreshIndependently(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls4, calls6 atomic.Int32
|
|
ipv6Available := false
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
calls4.Add(1)
|
|
return publicIPResponse(200, "192.0.2.10")
|
|
}
|
|
calls6.Add(1)
|
|
if !ipv6Available {
|
|
return publicIPResponse(503, "")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::20")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "N/A")
|
|
ipv6Available = true
|
|
time.Sleep(time.Minute)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::20")
|
|
if calls4.Load() != 1 || calls6.Load() != 3 {
|
|
t.Fatalf("family requests IPv4=%d IPv6=%d", calls4.Load(), calls6.Load())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPublicIPRefreshIsSingleFlightAndKeepsCachedReadsAvailable(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls atomic.Int32
|
|
slow := false
|
|
release := make(chan struct{})
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
if slow {
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return nil, r.Context().Err()
|
|
}
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, "192.0.2.10")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::10")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
slow = true
|
|
time.Sleep(5 * time.Minute)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
var wg sync.WaitGroup
|
|
var completed atomic.Int32
|
|
for range 20 {
|
|
wg.Go(func() { svc.resolvePublicIPsInBackground(); svc.resolvePublicIPs(); completed.Add(1) })
|
|
}
|
|
synctest.Wait()
|
|
// Cached reads must remain available while the HTTP request is blocked.
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::10")
|
|
count := calls.Load()
|
|
early := completed.Load()
|
|
close(release)
|
|
wg.Wait()
|
|
synctest.Wait()
|
|
if early != 20 {
|
|
t.Fatalf("only %d warm callers returned while the lookup was blocked", early)
|
|
}
|
|
if completed.Load() != 20 {
|
|
t.Fatalf("only %d synchronous callers completed", completed.Load())
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("concurrent refresh started %d requests, want 3 including initial pair", count)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPublicIPStatusUsesRefreshedAddressWithoutBlocking(t *testing.T) {
|
|
dbDir := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", dbDir)
|
|
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
|
savedHistory := systemMetrics
|
|
systemMetrics = newMetricHistory()
|
|
t.Cleanup(func() { systemMetrics = savedHistory })
|
|
synctest.Test(t, func(t *testing.T) {
|
|
value4, value6 := "192.0.2.10", "2001:db8::10"
|
|
slow := false
|
|
release := make(chan struct{})
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if slow {
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return nil, r.Context().Err()
|
|
}
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, value4)
|
|
}
|
|
return publicIPResponse(200, value6)
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
svc.RefreshStatus()
|
|
assertStatus := func(want string) {
|
|
t.Helper()
|
|
data, err := json.Marshal(svc.CurrentStatus())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var wire struct {
|
|
PublicIP struct {
|
|
IPv4 string `json:"ipv4"`
|
|
} `json:"publicIP"`
|
|
}
|
|
if err := json.Unmarshal(data, &wire); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wire.PublicIP.IPv4 != want {
|
|
t.Fatalf("status JSON IPv4=%q, want %q", wire.PublicIP.IPv4, want)
|
|
}
|
|
}
|
|
assertStatus(value4)
|
|
value4, value6, slow = "192.0.2.20", "2001:db8::20", true
|
|
time.Sleep(5 * time.Minute)
|
|
start := time.Now()
|
|
svc.RefreshStatus()
|
|
if time.Since(start) != 0 {
|
|
t.Fatal("status sample waited for a public-IP request")
|
|
}
|
|
synctest.Wait()
|
|
assertStatus("192.0.2.10")
|
|
close(release)
|
|
synctest.Wait()
|
|
svc.RefreshStatus()
|
|
assertStatus("192.0.2.20")
|
|
})
|
|
}
|
|
|
|
func TestPublicIPColdSynchronousCallersShareInitialLookup(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls, completed atomic.Int32
|
|
release := make(chan struct{})
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return nil, r.Context().Err()
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, "192.0.2.10")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::10")
|
|
})
|
|
svc := &ServerService{}
|
|
var wg sync.WaitGroup
|
|
for range 20 {
|
|
wg.Go(func() { svc.resolvePublicIPs(); completed.Add(1) })
|
|
}
|
|
synctest.Wait()
|
|
early, started := completed.Load(), calls.Load()
|
|
close(release)
|
|
wg.Wait()
|
|
synctest.Wait()
|
|
if early != 0 || started != 1 {
|
|
t.Fatalf("cold callers completed=%d started requests=%d, want 0 and 1", early, started)
|
|
}
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::10")
|
|
if calls.Load() != 2 || completed.Load() != 20 {
|
|
t.Fatalf("after initial lookup requests=%d completed=%d", calls.Load(), completed.Load())
|
|
}
|
|
})
|
|
}
|
|
|
|
func waitForPublicIPLookup(t *testing.T, svc *ServerService) {
|
|
t.Helper()
|
|
if done := svc.resolvePublicIPsInBackground(); done != nil {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func TestPublicIPv6FailuresBackOffAndCapRetries(t *testing.T) {
|
|
for _, failure := range []string{"http-error", "empty-body", "transport-error", "timeout"} {
|
|
t.Run(failure, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls6 atomic.Int32
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(http.StatusOK, "192.0.2.10")
|
|
}
|
|
calls6.Add(1)
|
|
switch failure {
|
|
case "empty-body":
|
|
return publicIPResponse(http.StatusOK, "")
|
|
case "transport-error":
|
|
return nil, io.ErrUnexpectedEOF
|
|
case "timeout":
|
|
<-r.Context().Done()
|
|
return nil, r.Context().Err()
|
|
default:
|
|
return publicIPResponse(http.StatusServiceUnavailable, "")
|
|
}
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "N/A")
|
|
if got := calls6.Load(); got != 2 {
|
|
t.Fatalf("initial IPv6 requests=%d, want 2", got)
|
|
}
|
|
for _, delay := range []time.Duration{time.Minute, 2 * time.Minute, 5 * time.Minute, 15 * time.Minute, 15 * time.Minute, 15 * time.Minute} {
|
|
before := calls6.Load()
|
|
time.Sleep(delay - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before {
|
|
t.Fatalf("IPv6 retried before %v: requests=%d, want %d", delay, got, before)
|
|
}
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before+2 {
|
|
t.Fatalf("IPv6 requests at %v=%d, want %d", delay, got, before+2)
|
|
}
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "N/A")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublicIPv6RecoveryResetsBackoffAndRefreshesNormally(t *testing.T) {
|
|
for _, recovered := range []string{"2001:db8::10", "2001:db8::20"} {
|
|
t.Run(recovered, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls6 atomic.Int32
|
|
available := true
|
|
value6 := "2001:db8::10"
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(http.StatusOK, "192.0.2.10")
|
|
}
|
|
calls6.Add(1)
|
|
if !available {
|
|
return publicIPResponse(http.StatusServiceUnavailable, "")
|
|
}
|
|
return publicIPResponse(http.StatusOK, value6)
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
available = false
|
|
for _, delay := range []time.Duration{5 * time.Minute, time.Minute, 2 * time.Minute, 5 * time.Minute} {
|
|
time.Sleep(delay)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::10")
|
|
}
|
|
available, value6 = true, recovered
|
|
before := calls6.Load()
|
|
time.Sleep(15*time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before {
|
|
t.Fatalf("recovery attempted before capped deadline: requests=%d, want %d", got, before)
|
|
}
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", recovered)
|
|
if got := calls6.Load(); got != before+1 {
|
|
t.Fatalf("recovery requests=%d, want %d", got, before+1)
|
|
}
|
|
|
|
before = calls6.Load()
|
|
time.Sleep(5*time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before {
|
|
t.Fatalf("successful IPv6 refreshed before five minutes: requests=%d, want %d", got, before)
|
|
}
|
|
available = false
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before+2 {
|
|
t.Fatalf("refresh requests=%d, want %d", got, before+2)
|
|
}
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", recovered)
|
|
before = calls6.Load()
|
|
time.Sleep(time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before {
|
|
t.Fatalf("IPv6 retried before reset one-minute deadline: requests=%d, want %d", got, before)
|
|
}
|
|
available, value6 = true, "2001:db8::30"
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", value6)
|
|
if got := calls6.Load(); got != before+1 {
|
|
t.Fatalf("one-minute recovery requests=%d, want %d", got, before+1)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublicIPv6BackoffPreservesIPv4RefreshAndRetry(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls4, calls6 atomic.Int32
|
|
available4, value4 := true, "192.0.2.10"
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
calls4.Add(1)
|
|
if available4 {
|
|
return publicIPResponse(http.StatusOK, value4)
|
|
}
|
|
} else {
|
|
calls6.Add(1)
|
|
}
|
|
return publicIPResponse(http.StatusServiceUnavailable, "")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
for _, delay := range []time.Duration{time.Minute, 2 * time.Minute, 5 * time.Minute} {
|
|
time.Sleep(delay)
|
|
waitForPublicIPLookup(t, svc)
|
|
}
|
|
if got := calls6.Load(); got != 8 {
|
|
t.Fatalf("IPv6 requests before capped backoff=%d, want 8", got)
|
|
}
|
|
value4 = "192.0.2.20"
|
|
before := calls4.Load()
|
|
time.Sleep(5*time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls4.Load(); got != before {
|
|
t.Fatalf("IPv4 refreshed before five minutes: requests=%d, want %d", got, before)
|
|
}
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, value4, "N/A")
|
|
if got := calls4.Load(); got != before+1 {
|
|
t.Fatalf("IPv4 refresh requests=%d, want %d", got, before+1)
|
|
}
|
|
available4 = false
|
|
time.Sleep(5 * time.Minute)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, value4, "N/A")
|
|
before = calls4.Load()
|
|
time.Sleep(time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls4.Load(); got != before {
|
|
t.Fatalf("IPv4 retried before one minute: requests=%d, want %d", got, before)
|
|
}
|
|
available4, value4 = true, "192.0.2.30"
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, value4, "N/A")
|
|
if got := calls4.Load(); got != before+1 {
|
|
t.Fatalf("IPv4 retry requests=%d, want %d", got, before+1)
|
|
}
|
|
if got := calls6.Load(); got != 8 {
|
|
t.Fatalf("IPv6 retried during capped backoff: requests=%d, want 8", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// The IP echo services are third parties; their answer is cached for the life
|
|
// of the process and copied into every status sample.
|
|
func TestGetPublicIPAcceptsOnlyAnAddress(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
want string
|
|
}{
|
|
{name: "ipv4 with line ending", body: "203.0.113.7\n", want: "203.0.113.7"},
|
|
{name: "ipv6", body: "2001:db8::7", want: "2001:db8::7"},
|
|
{name: "not an address", body: "<html>blocked</html>", want: "N/A"},
|
|
{name: "address followed by megabytes", body: "203.0.113.7\n" + strings.Repeat("1", 8<<20), want: "N/A"},
|
|
{name: "empty", body: "", want: "N/A"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(tt.body))
|
|
}))
|
|
defer server.Close()
|
|
if got := getPublicIP(server.URL); got != tt.want {
|
|
t.Fatalf("getPublicIP = %.40q (%d bytes), want %q", got, len(got), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|