ipv6 network test

This commit is contained in:
Ben Sarmiento
2024-06-16 06:36:44 +00:00
parent e50806d8e1
commit 5aea569be7
3 changed files with 153 additions and 178 deletions

View File

@@ -52,22 +52,16 @@ func MainApp(configPath string) {
os.Exit(1) os.Exit(1)
} }
repo := http.NewIPRepository(log.Named("network_test")) repoClient4 := http.NewHTTPClient("", 0, 1, false, config, log.Named("network_test"))
repoClient := http.NewHTTPClient( repoClient6 := http.NewHTTPClient("", 0, 1, true, config, log.Named("network_test"))
"", repo := http.NewIPRepository(repoClient4, repoClient6, log.Named("network_test"))
0, repo.NetworkTest(true)
1,
true,
config,
log.Named("network_test"),
)
repo.NetworkTest(repoClient, false)
apiClient := http.NewHTTPClient( apiClient := http.NewHTTPClient(
config.GetToken(), config.GetToken(),
config.GetRetriesUntilFailed(), // default retries = 2 config.GetRetriesUntilFailed(), // default retries = 2
config.GetApiTimeoutSecs(), // default api timeout = 60 config.GetApiTimeoutSecs(), // default api timeout = 60
false, // ipv6 support is not needed for api client false, // no need for ipv6 support
config, config,
log.Named("api_client"), log.Named("api_client"),
) )
@@ -76,16 +70,16 @@ func MainApp(configPath string) {
config.GetToken(), config.GetToken(),
config.GetRetriesUntilFailed(), // default retries = 2 config.GetRetriesUntilFailed(), // default retries = 2
config.GetDownloadTimeoutSecs(), // default download timeout = 10 config.GetDownloadTimeoutSecs(), // default download timeout = 10
false, // this is also api client, so no ipv6 support false, // no need for ipv6 support
config, config,
log.Named("unrestrict_client"), log.Named("unrestrict_client"),
) )
downloadClient := http.NewHTTPClient( downloadClient := http.NewHTTPClient(
"", // no token required for download client "",
config.GetRetriesUntilFailed(), // config.GetRetriesUntilFailed(),
config.GetDownloadTimeoutSecs(), // config.GetDownloadTimeoutSecs(),
true, // set as download client config.ShouldForceIPv6(),
config, config,
log.Named("download_client"), log.Named("download_client"),
) )

View File

@@ -15,7 +15,6 @@ import (
"time" "time"
"github.com/debridmediamanager/zurg/internal/config" "github.com/debridmediamanager/zurg/internal/config"
"github.com/debridmediamanager/zurg/pkg/hosts"
"github.com/debridmediamanager/zurg/pkg/logutil" "github.com/debridmediamanager/zurg/pkg/logutil"
http_dialer "github.com/mwitkow/go-http-dialer" http_dialer "github.com/mwitkow/go-http-dialer"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
@@ -29,9 +28,8 @@ type HTTPClient struct {
timeoutSecs int timeoutSecs int
backoff func(attempt int) time.Duration backoff func(attempt int) time.Duration
bearerToken string bearerToken string
isDownloadClient bool
cfg config.ConfigInterface cfg config.ConfigInterface
ipv6 cmap.ConcurrentMap[string, string] dnsCache cmap.ConcurrentMap[string, string]
ipv6Hosts []string ipv6Hosts []string
log *logutil.Logger log *logutil.Logger
} }
@@ -49,7 +47,7 @@ func NewHTTPClient(
token string, token string,
maxRetries int, maxRetries int,
timeoutSecs int, timeoutSecs int,
isDownloadClient bool, forceIPv6 bool,
cfg config.ConfigInterface, cfg config.ConfigInterface,
log *logutil.Logger, log *logutil.Logger,
) *HTTPClient { ) *HTTPClient {
@@ -59,9 +57,9 @@ func NewHTTPClient(
maxRetries: maxRetries, maxRetries: maxRetries,
timeoutSecs: timeoutSecs, timeoutSecs: timeoutSecs,
backoff: backoffFunc, backoff: backoffFunc,
isDownloadClient: isDownloadClient,
cfg: cfg, cfg: cfg,
ipv6: cmap.New[string](), dnsCache: cmap.New[string](),
ipv6Hosts: []string{},
log: log, log: log,
} }
@@ -96,21 +94,11 @@ func NewHTTPClient(
}, },
} }
if cfg.ShouldForceIPv6() { if forceIPv6 {
// fetch IPv6 hosts
ipv6List, err := hosts.FetchHosts(hosts.IPV6)
if err != nil {
log.Warnf("Failed to fetch IPv6 hosts: %v", err)
// Decide if you want to return nil here or continue without IPv6
} else {
client.ipv6Hosts = ipv6List
log.Debugf("Fetched %d IPv6 hosts", len(ipv6List))
}
// replace the default dialer with a custom one that resolves hostnames to IPv6 addresses // replace the default dialer with a custom one that resolves hostnames to IPv6 addresses
client.client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { client.client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
// if address is already cached, use it // if address is already cached, use it
if ipv6Address, ok := client.ipv6.Get(address); ok { if ipv6Address, ok := client.dnsCache.Get(address); ok {
return dialer.Dial(network, ipv6Address) return dialer.Dial(network, ipv6Address)
} }
@@ -126,26 +114,15 @@ func NewHTTPClient(
for _, ip := range ips { for _, ip := range ips {
if ip.IP.To4() == nil { // IPv6 address found if ip.IP.To4() == nil { // IPv6 address found
ipv6Address := net.JoinHostPort(ip.IP.String(), port) ipv6Address := net.JoinHostPort(ip.IP.String(), port)
client.ipv6.Set(address, ipv6Address) client.dnsCache.Set(address, ipv6Address)
return dialer.Dial(network, ipv6Address) return dialer.Dial(network, ipv6Address)
} }
} }
// no IPv6 address found, use the original address return nil, fmt.Errorf("no ipv6 address found")
log.Warnf("No IPv6 address found for host %s", host)
for _, ip := range ips {
if ip.IP.To4() != nil { // IPv4 address found
ipV4Address := net.JoinHostPort(ip.IP.String(), port)
client.ipv6.Set(address, ipV4Address)
return dialer.Dial(network, ipV4Address)
} }
} }
return dialer.Dial(network, address)
}
}
return &client return &client
} }
@@ -171,7 +148,7 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) {
resp.Body.Close() resp.Body.Close()
} }
r.replaceWithIPv6Host(req) // needed for ipv6 // r.optimizeHost(req)
resp, err = r.client.Do(req) resp, err = r.client.Do(req)
@@ -210,12 +187,7 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) {
return resp, err return resp, err
} }
func (r *HTTPClient) replaceWithIPv6Host(req *http.Request) { func (r *HTTPClient) optimizeHost(req *http.Request) {
// don't replace host if IPv6 is not supported or not forced
if !r.isDownloadClient || !r.cfg.ShouldForceIPv6() {
return
}
// this host should be replaced
if !strings.HasSuffix(req.Host, ".download.real-debrid.com") { if !strings.HasSuffix(req.Host, ".download.real-debrid.com") {
return return
} }
@@ -233,7 +205,6 @@ func (r *HTTPClient) replaceWithIPv6Host(req *http.Request) {
req.Host = r.ipv6Hosts[rand.Intn(len(r.ipv6Hosts))] req.Host = r.ipv6Hosts[rand.Intn(len(r.ipv6Hosts))]
req.URL.Host = req.Host req.URL.Host = req.Host
r.log.Debugf("Host %s is not a valid IPv6 host, assigning a random IPv6 host: %s", req.URL.Host, req.Host) r.log.Debugf("Host %s is not a valid IPv6 host, assigning a random IPv6 host: %s", req.URL.Host, req.Host)
// if !found && !r.CanFetchFirstByte(req.URL.String()) {
} }
func (r *HTTPClient) proxyDialer(proxyURL *url.URL) (proxy.Dialer, error) { func (r *HTTPClient) proxyDialer(proxyURL *url.URL) (proxy.Dialer, error) {

View File

@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
"net"
"net/http" "net/http"
"os" "os"
"time" "time"
@@ -15,113 +14,126 @@ import (
) )
type IPRepository struct { type IPRepository struct {
ipv4 []string ipv4client *HTTPClient
ipv6 []string ipv6client *HTTPClient
latencyMap map[string]float64 ipv4latencyMap map[string]float64
ipv6latencyMap map[string]float64
log *logutil.Logger log *logutil.Logger
} }
func NewIPRepository(log *logutil.Logger) *IPRepository { func NewIPRepository(ipv4client *HTTPClient, ipv6client *HTTPClient, log *logutil.Logger) *IPRepository {
repo := &IPRepository{ repo := &IPRepository{
ipv4: []string{}, ipv4client: ipv4client,
ipv6: []string{}, ipv6client: ipv6client,
latencyMap: make(map[string]float64), ipv4latencyMap: make(map[string]float64),
ipv6latencyMap: make(map[string]float64),
log: log, log: log,
} }
repo.lookupDomains()
return repo return repo
} }
func (r *IPRepository) NetworkTest(downloadClient *HTTPClient, forceRun bool) { func (r *IPRepository) NetworkTest(forceRun bool) {
latencyFile := "data/latency.json" ipv4latencyFile := "data/latency4.json"
ipv6latencyFile := "data/latency6.json"
if !forceRun { if !forceRun {
latencyData := r.readLatencyFile(latencyFile) latencyData := r.readLatencyFile(ipv4latencyFile)
if latencyData != nil { if latencyData != nil {
r.latencyMap = *latencyData r.ipv4latencyMap = *latencyData
return }
latencyData = r.readLatencyFile(ipv6latencyFile)
if latencyData != nil {
r.ipv6latencyMap = *latencyData
} }
} }
r.log.Info("Network test will start now. Note that it will only run once and record the latency of each domain for future use.") r.log.Info("Network test will start now. IGNORE THE WARNINGS!")
r.latencyTest(downloadClient) r.runLatencyTest()
r.log.Infof("Network test completed. Saving the results to %s", latencyFile) r.log.Infof("Network test completed. Saving the results to %s and %s", ipv4latencyFile, ipv6latencyFile)
r.writeLatencyFile(latencyFile) r.log.Debugf("ipv4 %v", r.ipv4latencyMap)
r.log.Debugf("ipv6 %v", r.ipv6latencyMap)
r.writeLatencyFile(ipv4latencyFile, r.ipv4latencyMap)
r.writeLatencyFile(ipv6latencyFile, r.ipv6latencyMap)
} }
func (r *IPRepository) lookupDomains() { func (r *IPRepository) runLatencyTest() {
limit := 99 limit := 99
increment := 10
start := 0 start := 0
for { for {
lastDomainWorked := false lastDomainsWorked := false
for i := start; i <= limit; i++ { for i := start; i <= limit; i++ {
domain := fmt.Sprintf("%d.download.real-debrid.com", i) domain := fmt.Sprintf("%d.download.real-debrid.com", i)
ips, err := net.LookupIP(domain) // ips, err := net.LookupIP(domain)
if err == nil && len(ips) > 0 { // if err != nil || len(ips) == 0 {
hasIPv6 := false // continue
for _, ip := range ips { // }
if ip.To4() == nil {
hasIPv6 = true latency, err := r.testDomainLatency(r.ipv4client, domain)
} if err == nil {
} r.ipv4latencyMap[domain] = latency
// assume it always has ipv4 r.log.Debugf("Latency from ipv4 %s: %.5f seconds", domain, latency)
r.ipv4 = append(r.ipv4, domain) if i >= limit-2 {
if hasIPv6 { lastDomainsWorked = true
r.ipv6 = append(r.ipv6, domain)
}
if i == limit {
lastDomainWorked = true
} }
} }
domain2 := fmt.Sprintf("%d.download.real-debrid.cloud", i) latency, err = r.testDomainLatency(r.ipv6client, domain)
ips2, err := net.LookupIP(domain2) if err == nil {
if err == nil && len(ips2) > 0 { r.ipv6latencyMap[domain] = latency
hasIPv6 := false r.log.Debugf("Latency from ipv6 %s: %.5f seconds", domain, latency)
for _, ip := range ips { if i >= limit-2 {
if ip.To4() == nil { lastDomainsWorked = true
hasIPv6 = true
} }
} }
r.ipv4 = append(r.ipv4, domain2)
if hasIPv6 { domain = fmt.Sprintf("%d.download.real-debrid.cloud", i)
r.ipv6 = append(r.ipv6, domain2) // ips, err = net.LookupIP(domain)
// if err != nil || len(ips) == 0 {
// continue
// }
latency, err = r.testDomainLatency(r.ipv4client, domain)
if err == nil {
r.ipv4latencyMap[domain] = latency
r.log.Debugf("Latency from ipv4 %s: %.5f seconds", domain, latency)
if i >= limit-2 {
lastDomainsWorked = true
} }
if i == limit { }
lastDomainWorked = true
latency, err = r.testDomainLatency(r.ipv6client, domain)
if err == nil {
r.ipv6latencyMap[domain] = latency
r.log.Debugf("Latency from ipv6 %s: %.5f seconds", domain, latency)
if i >= limit-2 {
lastDomainsWorked = true
} }
} }
} }
if lastDomainWorked { if lastDomainsWorked {
start = limit + 1 start = limit + 1
limit += increment limit += 10
} else { } else {
break break
} }
} }
r.log.Infof("Found %d IPv4 domains and %d IPv6 domains", len(r.ipv4), len(r.ipv6))
} }
func (r *IPRepository) latencyTest(downloadClient *HTTPClient) { func (r *IPRepository) testDomainLatency(client *HTTPClient, domain string) (float64, error) {
const testFileSize = 1 const testFileSize = 1 // byte
const iterations = 3 const iterations = 3
for _, domain := range r.ipv4 {
url := fmt.Sprintf("https://%s/speedtest/test.rar/%f", domain, rand.Float64()) url := fmt.Sprintf("https://%s/speedtest/test.rar/%f", domain, rand.Float64())
var totalDuration float64 var totalDuration float64
hasError := false var retErr error
for i := 0; i < iterations; i++ { for i := 0; i < iterations; i++ {
ctx, cancel := context.WithTimeout(context.Background(), iterations*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
r.log.Warnf("Failed to create request for %s: %v", domain, err) r.log.Warnf("Failed to create request for %s: %v", domain, err)
hasError = true retErr = err
break break
} }
@@ -130,10 +142,10 @@ func (r *IPRepository) latencyTest(downloadClient *HTTPClient) {
req.Header = headers req.Header = headers
start := time.Now() start := time.Now()
resp, err := downloadClient.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
r.log.Warnf("Failed to download from %s: %v", domain, err) r.log.Warnf("Failed to download from %s: %v", domain, err)
hasError = true retErr = err
break break
} }
@@ -144,21 +156,19 @@ func (r *IPRepository) latencyTest(downloadClient *HTTPClient) {
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
r.log.Warnf("Failed to read from %s: %v", domain, err) r.log.Warnf("Failed to read from %s: %v", domain, err)
hasError = true retErr = err
break break
} }
duration := time.Since(start).Seconds() duration := time.Since(start).Seconds()
totalDuration += duration totalDuration += duration
} }
if hasError { if retErr != nil {
continue return 0, retErr
} }
r.latencyMap[domain] = totalDuration / 3 avgDuration := totalDuration / 3
return avgDuration, nil
r.log.Debugf("Latency from %s: %.5f seconds", domain, r.latencyMap[domain])
}
} }
func (r *IPRepository) readLatencyFile(latencyFile string) *map[string]float64 { func (r *IPRepository) readLatencyFile(latencyFile string) *map[string]float64 {
@@ -172,16 +182,16 @@ func (r *IPRepository) readLatencyFile(latencyFile string) *map[string]float64 {
if err != nil { if err != nil {
return nil return nil
} }
var latencyMap map[string]float64 var ipv4latencyMap map[string]float64
if err := json.Unmarshal(jsonData, &latencyMap); err != nil { if err := json.Unmarshal(jsonData, &ipv4latencyMap); err != nil {
return nil return nil
} }
return &latencyMap return &ipv4latencyMap
} }
return nil return nil
} }
func (r *IPRepository) writeLatencyFile(latencyFile string) { func (r *IPRepository) writeLatencyFile(latencyFile string, data interface{}) {
file, err := os.Create(latencyFile) file, err := os.Create(latencyFile)
if err != nil { if err != nil {
r.log.Warnf("Cannot create latency file %s: %v", latencyFile, err) r.log.Warnf("Cannot create latency file %s: %v", latencyFile, err)
@@ -189,7 +199,7 @@ func (r *IPRepository) writeLatencyFile(latencyFile string) {
} }
defer file.Close() defer file.Close()
jsonData, err := json.Marshal(r.latencyMap) jsonData, err := json.Marshal(data)
if err != nil { if err != nil {
r.log.Warnf("Cannot marshal latency map: %v", err) r.log.Warnf("Cannot marshal latency map: %v", err)
return return