Add local-port and public-tunnel reachability checks
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RouteCheck is the result of checking one route's local target and its
|
||||
// public TLS path.
|
||||
type RouteCheck struct {
|
||||
Route string
|
||||
LocalUp bool
|
||||
PublicUp bool
|
||||
LocalError string
|
||||
PublicError string
|
||||
Checked time.Time
|
||||
}
|
||||
|
||||
// CheckLocal dials the route target's TCP port.
|
||||
func CheckLocal(ctx context.Context, target string) (bool, string) {
|
||||
dialer := net.Dialer{Timeout: 3 * time.Second}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
conn.Close()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// CheckPublic performs a TLS handshake against the public route hostname. It
|
||||
// goes through DNS, the public proxy/relay, the bridge and the client's TLS
|
||||
// certificate, so success means the external path is up. It intentionally
|
||||
// does not send application data, so it works for non-HTTP routes too.
|
||||
func CheckPublic(ctx context.Context, publicURL string) (bool, string) {
|
||||
u, err := url.Parse(publicURL)
|
||||
if err != nil || u.Hostname() == "" {
|
||||
return false, "no public hostname"
|
||||
}
|
||||
host := u.Hostname()
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
port = "443"
|
||||
}
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
raw, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
defer raw.Close()
|
||||
// A TCP connect can succeed while the TLS handshake stalls (for example
|
||||
// when the tunnel has no bridge); bound it independently of the dialer.
|
||||
_ = raw.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
tlsConn := tls.Client(raw, &tls.Config{
|
||||
ServerName: host,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
})
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// CheckRoute checks one route.
|
||||
func CheckRoute(ctx context.Context, cfg *Config, name string) RouteCheck {
|
||||
result := RouteCheck{Route: name, Checked: time.Now()}
|
||||
route, ok := cfg.Routes[name]
|
||||
if !ok {
|
||||
result.LocalError = "route not found"
|
||||
result.PublicError = "route not found"
|
||||
return result
|
||||
}
|
||||
result.LocalUp, result.LocalError = CheckLocal(ctx, route.Target)
|
||||
result.PublicUp, result.PublicError = CheckPublic(ctx, cfg.RouteURL(name))
|
||||
return result
|
||||
}
|
||||
|
||||
// CheckAll checks every route concurrently.
|
||||
func CheckAll(ctx context.Context, cfg *Config) []RouteCheck {
|
||||
names := make([]string, 0, len(cfg.Routes))
|
||||
for name := range cfg.Routes {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
results := make([]RouteCheck, len(names))
|
||||
var wg sync.WaitGroup
|
||||
for i, name := range names {
|
||||
wg.Add(1)
|
||||
go func(i int, name string) {
|
||||
defer wg.Done()
|
||||
results[i] = CheckRoute(ctx, cfg, name)
|
||||
}(i, name)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// CheckSummary formats the counts for a UI.
|
||||
func CheckSummary(results []RouteCheck) string {
|
||||
local, public := 0, 0
|
||||
for _, result := range results {
|
||||
if result.LocalUp {
|
||||
local++
|
||||
}
|
||||
if result.PublicUp {
|
||||
public++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("local %d/%d up · public %d/%d up", local, len(results), public, len(results))
|
||||
}
|
||||
Reference in New Issue
Block a user