Files
portmaster/spn/patrol/http.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
}