mirror of
https://github.com/qdm12/gluetun.git
synced 2026-05-06 20:10:11 +02:00
feat(dns): add leak check report log
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand/v2"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func leakCheck(ctx context.Context, client *http.Client) (report string, err error) {
|
||||||
|
const sessionLength = 40
|
||||||
|
session := generateRandomString(sessionLength)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
type result struct {
|
||||||
|
dnsToCount map[string]uint
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
resultsCh := make(chan result)
|
||||||
|
|
||||||
|
const requestsCount = 5
|
||||||
|
for range requestsCount {
|
||||||
|
go func() {
|
||||||
|
dnsToCount, err := triggerDNSQuery(ctx, client, session)
|
||||||
|
resultsCh <- result{dnsToCount: dnsToCount, err: err}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsToCount := make(map[string]uint)
|
||||||
|
for range requestsCount {
|
||||||
|
result := <-resultsCh
|
||||||
|
if result.err != nil {
|
||||||
|
if err == nil {
|
||||||
|
cancel()
|
||||||
|
err = fmt.Errorf("request failed: %w", result.err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for dns, count := range result.dnsToCount {
|
||||||
|
dnsToCount[dns] += count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatPercentages(dnsToCount), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomString(length uint) string {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
b := make([]byte, length)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = charset[rand.IntN(len(charset))] //nolint:gosec
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errIPLeakSessionMismatch = errors.New("ipleak.net session mismatch")
|
||||||
|
|
||||||
|
func triggerDNSQuery(ctx context.Context, client *http.Client, session string) (
|
||||||
|
dnsToCount map[string]uint, err error,
|
||||||
|
) {
|
||||||
|
const randomLength = 12
|
||||||
|
randomPart := generateRandomString(randomLength)
|
||||||
|
url := fmt.Sprintf("https://%s-%s.ipleak.net/dnsdetection/", session, randomPart)
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("performing request: %w", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
type ipLeakData struct {
|
||||||
|
Session string `json:"session"`
|
||||||
|
IP map[string]uint `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(response.Body)
|
||||||
|
var data ipLeakData
|
||||||
|
err = decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decoding response: %w", err)
|
||||||
|
} else if data.Session != session {
|
||||||
|
return nil, fmt.Errorf("%w: expected %s, got %s", errIPLeakSessionMismatch, session, data.Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.IP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPercentages(data map[string]uint) string {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var total uint
|
||||||
|
keys := make([]string, 0, len(data))
|
||||||
|
for k, v := range data {
|
||||||
|
total += v
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
if data[keys[i]] == data[keys[j]] {
|
||||||
|
return keys[i] < keys[j] // Tie-breaker: alphabetical
|
||||||
|
}
|
||||||
|
return data[keys[i]] > data[keys[j]]
|
||||||
|
})
|
||||||
|
|
||||||
|
results := make([]string, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
var pct float64
|
||||||
|
if total > 0 {
|
||||||
|
pct = math.Ceil((float64(data[key]) / float64(total)) * 100) //nolint:mnd
|
||||||
|
}
|
||||||
|
results[i] = fmt.Sprintf("%s (%.0f%%)", key, pct)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(results, ", ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_leakCheck(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const timeout = 10 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), timeout)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
client := http.DefaultClient
|
||||||
|
report, err := leakCheck(ctx, client)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, report)
|
||||||
|
}
|
||||||
@@ -59,6 +59,13 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
|
|
||||||
l.userTrigger = false
|
l.userTrigger = false
|
||||||
|
|
||||||
|
report, err := leakCheck(ctx, l.client)
|
||||||
|
if err != nil {
|
||||||
|
l.logger.Warnf("running leak check: %s", err)
|
||||||
|
} else {
|
||||||
|
l.logger.Infof("leak check report: %s", report)
|
||||||
|
}
|
||||||
|
|
||||||
exitLoop := l.runWait(ctx, runError)
|
exitLoop := l.runWait(ctx, runError)
|
||||||
if exitLoop {
|
if exitLoop {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user