187 lines
4.9 KiB
Go
187 lines
4.9 KiB
Go
package patrol
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/tevino/abool"
|
|
|
|
"github.com/safing/portbase/log"
|
|
"github.com/safing/portbase/modules"
|
|
"github.com/safing/portmaster/spn/conf"
|
|
)
|
|
|
|
var httpsConnectivityConfirmed = abool.NewBool(true)
|
|
|
|
// HTTPSConnectivityConfirmed returns whether the last HTTPS connectivity check succeeded.
|
|
// Is "true" before first test.
|
|
func HTTPSConnectivityConfirmed() bool {
|
|
return httpsConnectivityConfirmed.IsSet()
|
|
}
|
|
|
|
func connectivityCheckTask(ctx context.Context, task *modules.Task) error {
|
|
// Start tracing logs.
|
|
ctx, tracer := log.AddTracer(ctx)
|
|
defer tracer.Submit()
|
|
|
|
// Run checks and report status.
|
|
success := runConnectivityChecks(ctx)
|
|
if success {
|
|
tracer.Info("spn/patrol: all connectivity checks succeeded")
|
|
if httpsConnectivityConfirmed.SetToIf(false, true) {
|
|
module.TriggerEvent(ChangeSignalEventName, nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
tracer.Errorf("spn/patrol: connectivity check failed")
|
|
if httpsConnectivityConfirmed.SetToIf(true, false) {
|
|
module.TriggerEvent(ChangeSignalEventName, nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runConnectivityChecks(ctx context.Context) (ok bool) {
|
|
switch {
|
|
case conf.HubHasIPv4() && !runHTTPSConnectivityChecks(ctx, "tcp4"):
|
|
return false
|
|
case conf.HubHasIPv6() && !runHTTPSConnectivityChecks(ctx, "tcp6"):
|
|
return false
|
|
default:
|
|
// All checks passed.
|
|
return true
|
|
}
|
|
}
|
|
|
|
func runHTTPSConnectivityChecks(ctx context.Context, network string) (ok bool) {
|
|
// Step 1: Check 1 domain, require 100%
|
|
if checkHTTPSConnectivity(ctx, network, 1, 1) {
|
|
return true
|
|
}
|
|
|
|
// Step 2: Check 5 domains, require 80%
|
|
if checkHTTPSConnectivity(ctx, network, 5, 0.8) {
|
|
return true
|
|
}
|
|
|
|
// Step 3: Check 20 domains, require 70%
|
|
if checkHTTPSConnectivity(ctx, network, 20, 0.7) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func checkHTTPSConnectivity(ctx context.Context, network string, checks int, requiredSuccessFraction float32) (ok bool) {
|
|
log.Tracer(ctx).Tracef(
|
|
"spn/patrol: testing connectivity via https (%d checks; %.0f%% required)",
|
|
checks,
|
|
requiredSuccessFraction*100,
|
|
)
|
|
|
|
// Run tests.
|
|
var succeeded int
|
|
for i := 0; i < checks; i++ {
|
|
if checkHTTPSConnection(ctx, network) {
|
|
succeeded++
|
|
}
|
|
}
|
|
|
|
// Check success.
|
|
successFraction := float32(succeeded) / float32(checks)
|
|
if successFraction < requiredSuccessFraction {
|
|
log.Tracer(ctx).Warningf(
|
|
"spn/patrol: https/%s connectivity check failed: %d/%d (%.0f%%)",
|
|
network,
|
|
succeeded,
|
|
checks,
|
|
successFraction*100,
|
|
)
|
|
return false
|
|
}
|
|
|
|
log.Tracer(ctx).Debugf(
|
|
"spn/patrol: https/%s connectivity check succeeded: %d/%d (%.0f%%)",
|
|
network,
|
|
succeeded,
|
|
checks,
|
|
successFraction*100,
|
|
)
|
|
return true
|
|
}
|
|
|
|
func checkHTTPSConnection(ctx context.Context, network string) (ok bool) {
|
|
testDomain := getRandomTestDomain()
|
|
code, err := CheckHTTPSConnection(ctx, network, testDomain)
|
|
if err != nil {
|
|
log.Tracer(ctx).Debugf("spn/patrol: https/%s connect check failed: %s: %s", network, testDomain, err)
|
|
return false
|
|
}
|
|
|
|
log.Tracer(ctx).Tracef("spn/patrol: https/%s connect check succeeded: %s [%d]", network, testDomain, code)
|
|
return true
|
|
}
|
|
|
|
// CheckHTTPSConnection checks if a HTTPS connection to the given domain can be established.
|
|
func CheckHTTPSConnection(ctx context.Context, network, domain string) (statusCode int, err error) {
|
|
// Check network parameter.
|
|
switch network {
|
|
case "tcp4":
|
|
case "tcp6":
|
|
default:
|
|
return 0, fmt.Errorf("provided unsupported network: %s", network)
|
|
}
|
|
|
|
// Build URL.
|
|
// Use HTTPS to ensure that we have really communicated with the desired
|
|
// server and not with an intermediate.
|
|
url := fmt.Sprintf("https://%s/", domain)
|
|
|
|
// Prepare all parts of the request.
|
|
// TODO: Evaluate if we want to change the User-Agent.
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
dialer := &net.Dialer{
|
|
Timeout: 15 * time.Second,
|
|
LocalAddr: conf.GetBindAddr(network),
|
|
FallbackDelay: -1, // Disables Fast Fallback from IPv6 to IPv4.
|
|
KeepAlive: -1, // Disable keep-alive.
|
|
}
|
|
dialWithNet := func(ctx context.Context, _, addr string) (net.Conn, error) {
|
|
// Ignore network by http client.
|
|
// Instead, force either tcp4 or tcp6.
|
|
return dialer.DialContext(ctx, network, addr)
|
|
}
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: dialWithNet,
|
|
DisableKeepAlives: true,
|
|
DisableCompression: true,
|
|
TLSHandshakeTimeout: 15 * time.Second,
|
|
},
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// Make request to server.
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to send http request: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = resp.Body.Close()
|
|
}()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
|
return resp.StatusCode, fmt.Errorf("unexpected status code: %s", resp.Status)
|
|
}
|
|
|
|
return resp.StatusCode, nil
|
|
}
|