mirror of
https://github.com/fosrl/newt.git
synced 2026-08-09 13:59:07 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa637f4d8 | ||
|
|
1bde15f0a7 | ||
|
|
b5a7213cdb |
@@ -1,176 +0,0 @@
|
|||||||
// Package exitnode implements the exit-node ping dance run before
|
|
||||||
// registering with the server: request the candidate exit nodes, ping each
|
|
||||||
// one over HTTP, and report the results so the server can pick the best one.
|
|
||||||
// It is shared between newt and olm, which both register the same way.
|
|
||||||
package exitnode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fosrl/newt/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExitNodeData is the payload the server sends in response to a
|
|
||||||
// "*/ping/request" message.
|
|
||||||
type ExitNodeData struct {
|
|
||||||
ExitNodes []ExitNode `json:"exitNodes"`
|
|
||||||
ChainId string `json:"chainId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExitNode is a candidate exit node offered by the server for ping selection.
|
|
||||||
type ExitNode struct {
|
|
||||||
ID int `json:"exitNodeId"`
|
|
||||||
Name string `json:"exitNodeName"`
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
Weight float64 `json:"weight"`
|
|
||||||
WasPreviouslyConnected bool `json:"wasPreviouslyConnected"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExitNodePingResult is the measured latency (or error) for one exit node,
|
|
||||||
// sent back to the server in the "*/wg/register" message's pingResults field.
|
|
||||||
type ExitNodePingResult struct {
|
|
||||||
ExitNodeID int `json:"exitNodeId"`
|
|
||||||
LatencyMs int64 `json:"latencyMs"`
|
|
||||||
Weight float64 `json:"weight"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
Name string `json:"exitNodeName"`
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
WasPreviouslyConnected bool `json:"wasPreviouslyConnected"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PingExitNodes pings the given exit nodes over HTTP and returns a per-node
|
|
||||||
// ExitNodePingResult suitable for inclusion in a wg/register message's
|
|
||||||
// pingResults field, so the server can select the best exit node.
|
|
||||||
//
|
|
||||||
// If there's only one exit node, or preferEndpoint names one of them, the
|
|
||||||
// matching node is returned immediately with LatencyMs 0 and no pinging is
|
|
||||||
// done. Otherwise every node is pinged pingAttempts times over HTTP GET
|
|
||||||
// <endpoint>/ping and the average latency of successful attempts is used.
|
|
||||||
//
|
|
||||||
// When alreadyConnected is true, a node flagged WasPreviouslyConnected is
|
|
||||||
// excluded from the results as long as at least one other healthy node is
|
|
||||||
// available, biasing reconnects toward switching away from a possibly
|
|
||||||
// degraded node.
|
|
||||||
func PingExitNodes(exitNodes []ExitNode, preferEndpoint string, alreadyConnected bool) []ExitNodePingResult {
|
|
||||||
if len(exitNodes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(exitNodes) == 1 || preferEndpoint != "" {
|
|
||||||
selected := exitNodes[0]
|
|
||||||
if preferEndpoint != "" {
|
|
||||||
for _, node := range exitNodes {
|
|
||||||
if node.Endpoint == preferEndpoint {
|
|
||||||
selected = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug("Only one exit node available, using it directly: %s", selected.Endpoint)
|
|
||||||
|
|
||||||
return []ExitNodePingResult{
|
|
||||||
{
|
|
||||||
ExitNodeID: selected.ID,
|
|
||||||
LatencyMs: 0,
|
|
||||||
Weight: selected.Weight,
|
|
||||||
Error: "",
|
|
||||||
Name: selected.Name,
|
|
||||||
Endpoint: selected.Endpoint,
|
|
||||||
WasPreviouslyConnected: selected.WasPreviouslyConnected,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type nodeResult struct {
|
|
||||||
Node ExitNode
|
|
||||||
Latency time.Duration
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make([]nodeResult, len(exitNodes))
|
|
||||||
const pingAttempts = 3
|
|
||||||
for i, node := range exitNodes {
|
|
||||||
var totalLatency time.Duration
|
|
||||||
var lastErr error
|
|
||||||
successes := 0
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
url := node.Endpoint
|
|
||||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
|
||||||
url = "http://" + url
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(url, "/ping") {
|
|
||||||
url = strings.TrimRight(url, "/") + "/ping"
|
|
||||||
}
|
|
||||||
for j := 0; j < pingAttempts; j++ {
|
|
||||||
start := time.Now()
|
|
||||||
resp, err := httpClient.Get(url)
|
|
||||||
latency := time.Since(start)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
logger.Warn("Failed to ping exit node %d (%s) attempt %d: %v", node.ID, url, j+1, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
totalLatency += latency
|
|
||||||
successes++
|
|
||||||
}
|
|
||||||
var avgLatency time.Duration
|
|
||||||
if successes > 0 {
|
|
||||||
avgLatency = totalLatency / time.Duration(successes)
|
|
||||||
}
|
|
||||||
if successes == 0 {
|
|
||||||
results[i] = nodeResult{Node: node, Latency: 0, Err: lastErr}
|
|
||||||
} else {
|
|
||||||
results[i] = nodeResult{Node: node, Latency: avgLatency, Err: nil}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pingResults []ExitNodePingResult
|
|
||||||
for _, res := range results {
|
|
||||||
errMsg := ""
|
|
||||||
if res.Err != nil {
|
|
||||||
errMsg = res.Err.Error()
|
|
||||||
}
|
|
||||||
pingResults = append(pingResults, ExitNodePingResult{
|
|
||||||
ExitNodeID: res.Node.ID,
|
|
||||||
LatencyMs: res.Latency.Milliseconds(),
|
|
||||||
Weight: res.Node.Weight,
|
|
||||||
Error: errMsg,
|
|
||||||
Name: res.Node.Name,
|
|
||||||
Endpoint: res.Node.Endpoint,
|
|
||||||
WasPreviouslyConnected: res.Node.WasPreviouslyConnected,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if alreadyConnected {
|
|
||||||
var filteredPingResults []ExitNodePingResult
|
|
||||||
previouslyConnectedNodeIdx := -1
|
|
||||||
for i, res := range pingResults {
|
|
||||||
if res.WasPreviouslyConnected {
|
|
||||||
previouslyConnectedNodeIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
goodNodeCount := 0
|
|
||||||
for i, res := range pingResults {
|
|
||||||
if i != previouslyConnectedNodeIdx && res.LatencyMs > 0 && res.Error == "" {
|
|
||||||
goodNodeCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if previouslyConnectedNodeIdx != -1 && goodNodeCount > 0 {
|
|
||||||
for i, res := range pingResults {
|
|
||||||
if i != previouslyConnectedNodeIdx {
|
|
||||||
filteredPingResults = append(filteredPingResults, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pingResults = filteredPingResults
|
|
||||||
logger.Info("Excluding previously connected exit node from ping results due to other available nodes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pingResults
|
|
||||||
}
|
|
||||||
@@ -115,10 +115,6 @@ func FindUnusedUTUN() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func configureDarwin(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
func configureDarwin(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
||||||
if NativeConfigDisabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Configuring darwin interface: %s", interfaceName)
|
logger.Info("Configuring darwin interface: %s", interfaceName)
|
||||||
|
|
||||||
prefix, _ := ipNet.Mask.Size()
|
prefix, _ := ipNet.Mask.Size()
|
||||||
@@ -171,99 +167,3 @@ func configureLinux(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSecondaryAddress adds an additional IP address (given as CIDR, e.g. "10.10.0.5/32")
|
|
||||||
// to an already-configured interface. It also records the address in the shared
|
|
||||||
// NetworkSettings (see AddIPv4Address) so mobile (iOS/Android) packet-tunnel providers
|
|
||||||
// pick it up on their next settings poll - those platforms have no OS-level interface
|
|
||||||
// to configure directly, so this is the only way they learn about the address.
|
|
||||||
func AddSecondaryAddress(interfaceName string, addr string) error {
|
|
||||||
ip, ipNet, err := net.ParseCIDR(addr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid IP address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mask := net.IP(ipNet.Mask).String()
|
|
||||||
AddIPv4Address(ip.String(), mask)
|
|
||||||
|
|
||||||
if interfaceName == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
return configureLinux(interfaceName, ip, ipNet)
|
|
||||||
case "darwin":
|
|
||||||
return configureDarwin(interfaceName, ip, ipNet)
|
|
||||||
case "windows":
|
|
||||||
return configureWindows(interfaceName, ip, ipNet)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveSecondaryAddress removes an IP address (given as CIDR) previously added with
|
|
||||||
// AddSecondaryAddress, including from the shared NetworkSettings used by mobile
|
|
||||||
// packet-tunnel providers.
|
|
||||||
func RemoveSecondaryAddress(interfaceName string, addr string) error {
|
|
||||||
ip, ipNet, err := net.ParseCIDR(addr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid IP address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveIPv4Address(ip.String())
|
|
||||||
|
|
||||||
if interfaceName == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
return removeLinuxAddress(interfaceName, ip, ipNet)
|
|
||||||
case "darwin":
|
|
||||||
return removeDarwinAddress(interfaceName, ip, ipNet)
|
|
||||||
case "windows":
|
|
||||||
return removeWindowsAddress(interfaceName, ip, ipNet)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeLinuxAddress(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
|
||||||
link, err := netlink.LinkByName(interfaceName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get interface %s: %v", interfaceName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := &netlink.Addr{
|
|
||||||
IPNet: &net.IPNet{
|
|
||||||
IP: ip,
|
|
||||||
Mask: ipNet.Mask,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := netlink.AddrDel(link, addr); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove IP address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeDarwinAddress(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
|
||||||
if NativeConfigDisabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix, _ := ipNet.Mask.Size()
|
|
||||||
ipStr := fmt.Sprintf("%s/%d", ip.String(), prefix)
|
|
||||||
|
|
||||||
cmd := exec.Command("/sbin/ifconfig", interfaceName, "inet", ipStr, "-alias")
|
|
||||||
logger.Info("Running command: %v", cmd)
|
|
||||||
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ifconfig command failed: %v, output: %s", err, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,3 @@ import (
|
|||||||
func configureWindows(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
func configureWindows(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
||||||
return fmt.Errorf("configureWindows called on non-Windows platform")
|
return fmt.Errorf("configureWindows called on non-Windows platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWindowsAddress(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
|
||||||
return fmt.Errorf("removeWindowsAddress called on non-Windows platform")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -61,35 +61,3 @@ func configureWindows(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWindowsAddress(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
|
|
||||||
iface, err := net.InterfaceByName(interfaceName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get interface %s: %v", interfaceName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get LUID for interface %s: %v", interfaceName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
maskBits, _ := ipNet.Mask.Size()
|
|
||||||
|
|
||||||
var addr netip.Addr
|
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
addr, _ = netip.AddrFromSlice(ip4)
|
|
||||||
} else {
|
|
||||||
addr, _ = netip.AddrFromSlice(ip)
|
|
||||||
}
|
|
||||||
if !addr.IsValid() {
|
|
||||||
return fmt.Errorf("failed to convert IP address")
|
|
||||||
}
|
|
||||||
prefix := netip.PrefixFrom(addr, maskBits)
|
|
||||||
|
|
||||||
logger.Info("Removing IP address %s from interface %s", prefix.String(), interfaceName)
|
|
||||||
if err := luid.DeleteIPAddress(prefix); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove IP address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
+13
-109
@@ -32,20 +32,6 @@ const VPNRouteMetric = 9999
|
|||||||
// this to true (e.g. from a config value) before routes are added.
|
// this to true (e.g. from a config value) before routes are added.
|
||||||
var PreferLocalRoutes = false
|
var PreferLocalRoutes = false
|
||||||
|
|
||||||
// NativeConfigDisabled, when true, skips the raw `ifconfig`/`route` subprocess
|
|
||||||
// calls this package otherwise makes on darwin (configureDarwin,
|
|
||||||
// removeDarwinAddress, DarwinAddRouteWithSource, DarwinRemoveRoute) while still
|
|
||||||
// populating the JSON-facing NetworkSettings state. This must be set when the
|
|
||||||
// TUN device's addresses/routes are instead owned by an external mechanism
|
|
||||||
// that reconciles them independently - namely Apple's NetworkExtension
|
|
||||||
// (NEPacketTunnelProvider.setTunnelNetworkSettings), which is the sole
|
|
||||||
// sanctioned way to configure that virtual interface. Running our own
|
|
||||||
// ifconfig/route commands in addition to NE applying its own settings was
|
|
||||||
// observed to install two competing routes to the same destination (one via
|
|
||||||
// NE's gatewayAddress-based route, one via our own `-ifa` route), so the two
|
|
||||||
// mechanisms must be mutually exclusive rather than layered.
|
|
||||||
var NativeConfigDisabled = false
|
|
||||||
|
|
||||||
// DarwinAddRoute adds a route via the BSD routing table. Unlike Linux/Windows,
|
// DarwinAddRoute adds a route via the BSD routing table. Unlike Linux/Windows,
|
||||||
// BSD's routing table has no per-route metric - preference between an
|
// BSD's routing table has no per-route metric - preference between an
|
||||||
// overlapping local route and this VPN route is instead resolved by
|
// overlapping local route and this VPN route is instead resolved by
|
||||||
@@ -53,43 +39,22 @@ var NativeConfigDisabled = false
|
|||||||
// rather than replacing an existing route to the same destination, so a local
|
// rather than replacing an existing route to the same destination, so a local
|
||||||
// route is never displaced by one we add here.
|
// route is never displaced by one we add here.
|
||||||
func DarwinAddRoute(destination string, gateway string, interfaceName string) error {
|
func DarwinAddRoute(destination string, gateway string, interfaceName string) error {
|
||||||
return DarwinAddRouteWithSource(destination, gateway, interfaceName, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DarwinAddRouteWithSource is DarwinAddRoute with an explicit source address
|
|
||||||
// (route(8) `-ifa`). This is required when the interface carries more than
|
|
||||||
// one address (e.g. an exit node's secondary tunnel address alongside the
|
|
||||||
// site tunnel's primary address): without `-ifa`, BSD picks a source address
|
|
||||||
// for the route on its own - typically the interface's primary address - and
|
|
||||||
// WireGuard's own reverse-path filtering on the remote end will silently drop
|
|
||||||
// packets whose source doesn't match the peer's configured AllowedIPs, even
|
|
||||||
// though the tunnel/handshake itself stays up.
|
|
||||||
func DarwinAddRouteWithSource(destination string, gateway string, interfaceName string, sourceIP string) error {
|
|
||||||
if runtime.GOOS != "darwin" {
|
if runtime.GOOS != "darwin" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if NativeConfigDisabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var args []string
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
if gateway != "" {
|
if gateway != "" {
|
||||||
// Route with specific gateway
|
// Route with specific gateway
|
||||||
args = []string{"-q", "-n", "add", "-inet", destination, "-gateway", gateway}
|
cmd = exec.Command("route", "-q", "-n", "add", "-inet", destination, "-gateway", gateway)
|
||||||
} else if interfaceName != "" {
|
} else if interfaceName != "" {
|
||||||
// Route via interface
|
// Route via interface
|
||||||
args = []string{"-q", "-n", "add", "-inet", destination, "-interface", interfaceName}
|
cmd = exec.Command("route", "-q", "-n", "add", "-inet", destination, "-interface", interfaceName)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("either gateway or interface must be specified")
|
return fmt.Errorf("either gateway or interface must be specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sourceIP != "" {
|
|
||||||
args = append(args, "-ifa", sourceIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("route", args...)
|
|
||||||
|
|
||||||
logger.Info("Running command: %v", cmd)
|
logger.Info("Running command: %v", cmd)
|
||||||
|
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
@@ -104,9 +69,6 @@ func DarwinRemoveRoute(destination string) error {
|
|||||||
if runtime.GOOS != "darwin" {
|
if runtime.GOOS != "darwin" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if NativeConfigDisabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("route", "-q", "-n", "delete", "-inet", destination)
|
cmd := exec.Command("route", "-q", "-n", "delete", "-inet", destination)
|
||||||
logger.Info("Running command: %v", cmd)
|
logger.Info("Running command: %v", cmd)
|
||||||
@@ -211,30 +173,15 @@ func LinuxRemoveRoute(destination string, interfaceName string) error {
|
|||||||
|
|
||||||
// addRouteForServerIP adds an OS-specific route for the server IP
|
// addRouteForServerIP adds an OS-specific route for the server IP
|
||||||
func AddRouteForServerIP(serverIP, interfaceName string) error {
|
func AddRouteForServerIP(serverIP, interfaceName string) error {
|
||||||
return AddRouteForServerIPWithSource(serverIP, interfaceName, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRouteForServerIPWithSource is AddRouteForServerIP with an explicit source
|
|
||||||
// address for the darwin route (see DarwinAddRouteWithSource) - needed when
|
|
||||||
// the interface carries more than one address, e.g. an exit node connection
|
|
||||||
// where the interface's primary address belongs to the site tunnel rather
|
|
||||||
// than the exit node.
|
|
||||||
func AddRouteForServerIPWithSource(serverIP, interfaceName string, sourceIP string) error {
|
|
||||||
if interfaceName == "" {
|
if interfaceName == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the NetworkSettings entry (and its gatewayAddress, for the
|
|
||||||
// NetworkExtension source-pinning trick above) unconditionally, same as
|
|
||||||
// AddRoutesWithSource does for remote subnets - mobile packet-tunnel
|
|
||||||
// providers rely on this regardless of GOOS.
|
|
||||||
if err := AddRouteForNetworkConfigWithGateway(serverIP, sourceIP); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: does this also need to be ios?
|
// TODO: does this also need to be ios?
|
||||||
if runtime.GOOS == "darwin" { // macos requires routes for each peer to be added but this messes with other platforms
|
if runtime.GOOS == "darwin" { // macos requires routes for each peer to be added but this messes with other platforms
|
||||||
return DarwinAddRouteWithSource(serverIP, "", interfaceName, sourceIP)
|
if err := AddRouteForNetworkConfig(serverIP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return DarwinAddRoute(serverIP, "", interfaceName)
|
||||||
}
|
}
|
||||||
// else if runtime.GOOS == "windows" {
|
// else if runtime.GOOS == "windows" {
|
||||||
// return WindowsAddRoute(serverIP, "", interfaceName)
|
// return WindowsAddRoute(serverIP, "", interfaceName)
|
||||||
@@ -246,24 +193,14 @@ func AddRouteForServerIPWithSource(serverIP, interfaceName string, sourceIP stri
|
|||||||
|
|
||||||
// removeRouteForServerIP removes an OS-specific route for the server IP
|
// removeRouteForServerIP removes an OS-specific route for the server IP
|
||||||
func RemoveRouteForServerIP(serverIP string, interfaceName string) error {
|
func RemoveRouteForServerIP(serverIP string, interfaceName string) error {
|
||||||
return RemoveRouteForServerIPWithSource(serverIP, interfaceName, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveRouteForServerIPWithSource is RemoveRouteForServerIP with an explicit
|
|
||||||
// source/gateway address - must match whatever was passed to
|
|
||||||
// AddRouteForServerIPWithSource when the route was added (see
|
|
||||||
// RemoveRouteForNetworkConfigWithGateway).
|
|
||||||
func RemoveRouteForServerIPWithSource(serverIP string, interfaceName string, sourceIP string) error {
|
|
||||||
if interfaceName == "" {
|
if interfaceName == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RemoveRouteForNetworkConfigWithGateway(serverIP, sourceIP); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: does this also need to be ios?
|
// TODO: does this also need to be ios?
|
||||||
if runtime.GOOS == "darwin" { // macos requires routes for each peer to be added but this messes with other platforms
|
if runtime.GOOS == "darwin" { // macos requires routes for each peer to be added but this messes with other platforms
|
||||||
|
if err := RemoveRouteForNetworkConfig(serverIP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return DarwinRemoveRoute(serverIP)
|
return DarwinRemoveRoute(serverIP)
|
||||||
}
|
}
|
||||||
// else if runtime.GOOS == "windows" {
|
// else if runtime.GOOS == "windows" {
|
||||||
@@ -275,20 +212,6 @@ func RemoveRouteForServerIPWithSource(serverIP string, interfaceName string, sou
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AddRouteForNetworkConfig(destination string) error {
|
func AddRouteForNetworkConfig(destination string) error {
|
||||||
return AddRouteForNetworkConfigWithGateway(destination, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRouteForNetworkConfigWithGateway is AddRouteForNetworkConfig with an
|
|
||||||
// explicit gateway address for the route entry surfaced via NetworkSettings.
|
|
||||||
// This is consumed by mobile (iOS/macOS NetworkExtension) packet-tunnel
|
|
||||||
// providers as NEIPv4Route.gatewayAddress. NetworkExtension gives us no
|
|
||||||
// direct way to pin a route's source address (no equivalent of BSD's `route
|
|
||||||
// -ifa`) - but setting gatewayAddress to one of the tunnel interface's own
|
|
||||||
// addresses makes the OS resolve "how do I reach this gateway" recursively
|
|
||||||
// to that address/interface pairing, which is what determines the source
|
|
||||||
// address used for packets matching the route. This is the same underlying
|
|
||||||
// mechanism as `route add -gateway` (see DarwinAddRoute's gateway branch).
|
|
||||||
func AddRouteForNetworkConfigWithGateway(destination string, gateway string) error {
|
|
||||||
// Parse the subnet to extract IP and mask
|
// Parse the subnet to extract IP and mask
|
||||||
_, ipNet, err := net.ParseCIDR(destination)
|
_, ipNet, err := net.ParseCIDR(destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -299,21 +222,12 @@ func AddRouteForNetworkConfigWithGateway(destination string, gateway string) err
|
|||||||
mask := net.IP(ipNet.Mask).String()
|
mask := net.IP(ipNet.Mask).String()
|
||||||
destinationAddress := ipNet.IP.String()
|
destinationAddress := ipNet.IP.String()
|
||||||
|
|
||||||
AddIPv4IncludedRoute(IPv4Route{DestinationAddress: destinationAddress, SubnetMask: mask, GatewayAddress: gateway})
|
AddIPv4IncludedRoute(IPv4Route{DestinationAddress: destinationAddress, SubnetMask: mask})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveRouteForNetworkConfig(destination string) error {
|
func RemoveRouteForNetworkConfig(destination string) error {
|
||||||
return RemoveRouteForNetworkConfigWithGateway(destination, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveRouteForNetworkConfigWithGateway is RemoveRouteForNetworkConfig with
|
|
||||||
// an explicit gateway address. This must match whatever gateway the route was
|
|
||||||
// added with (see AddRouteForNetworkConfigWithGateway) - RemoveIPv4IncludedRoute
|
|
||||||
// matches by full struct equality, so a mismatched gateway means the entry is
|
|
||||||
// silently never found/removed.
|
|
||||||
func RemoveRouteForNetworkConfigWithGateway(destination string, gateway string) error {
|
|
||||||
// Parse the subnet to extract IP and mask
|
// Parse the subnet to extract IP and mask
|
||||||
_, ipNet, err := net.ParseCIDR(destination)
|
_, ipNet, err := net.ParseCIDR(destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -324,23 +238,13 @@ func RemoveRouteForNetworkConfigWithGateway(destination string, gateway string)
|
|||||||
mask := net.IP(ipNet.Mask).String()
|
mask := net.IP(ipNet.Mask).String()
|
||||||
destinationAddress := ipNet.IP.String()
|
destinationAddress := ipNet.IP.String()
|
||||||
|
|
||||||
RemoveIPv4IncludedRoute(IPv4Route{DestinationAddress: destinationAddress, SubnetMask: mask, GatewayAddress: gateway})
|
RemoveIPv4IncludedRoute(IPv4Route{DestinationAddress: destinationAddress, SubnetMask: mask})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addRoutes adds routes for each subnet in RemoteSubnets
|
// addRoutes adds routes for each subnet in RemoteSubnets
|
||||||
func AddRoutes(remoteSubnets []string, interfaceName string) error {
|
func AddRoutes(remoteSubnets []string, interfaceName string) error {
|
||||||
return AddRoutesWithSource(remoteSubnets, interfaceName, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRoutesWithSource is AddRoutes with an explicit source address for the
|
|
||||||
// darwin routes (see DarwinAddRouteWithSource) - needed when the interface
|
|
||||||
// carries more than one address (e.g. a site tunnel address alongside an
|
|
||||||
// exit node's secondary address), so the routes for these subnets are pinned
|
|
||||||
// to the address they actually belong to rather than whichever address
|
|
||||||
// darwin would otherwise default to.
|
|
||||||
func AddRoutesWithSource(remoteSubnets []string, interfaceName string, sourceIP string) error {
|
|
||||||
if len(remoteSubnets) == 0 {
|
if len(remoteSubnets) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -364,7 +268,7 @@ func AddRoutesWithSource(remoteSubnets []string, interfaceName string, sourceIP
|
|||||||
|
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
if err := DarwinAddRouteWithSource(subnet, "", interfaceName, sourceIP); err != nil {
|
if err := DarwinAddRoute(subnet, "", interfaceName); err != nil {
|
||||||
logger.Error("Failed to add Darwin route for subnet %s: %v", subnet, err)
|
logger.Error("Failed to add Darwin route for subnet %s: %v", subnet, err)
|
||||||
}
|
}
|
||||||
case "windows":
|
case "windows":
|
||||||
|
|||||||
@@ -81,45 +81,6 @@ func SetIPv4Settings(addresses []string, subnetMasks []string) {
|
|||||||
logger.Info("Set IPv4 addresses: %v, subnet masks: %v", addresses, subnetMasks)
|
logger.Info("Set IPv4 addresses: %v, subnet masks: %v", addresses, subnetMasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddIPv4Address appends an additional IPv4 address/subnet mask pair to the
|
|
||||||
// tunnel's network settings. This is how a secondary interface address gets
|
|
||||||
// exposed to mobile (iOS/Android) packet-tunnel providers, which read the
|
|
||||||
// full IPv4Addresses/IPv4SubnetMasks arrays (not just the first entry) and
|
|
||||||
// re-apply them on every settings poll.
|
|
||||||
func AddIPv4Address(address string, subnetMask string) {
|
|
||||||
networkSettingsMutex.Lock()
|
|
||||||
defer networkSettingsMutex.Unlock()
|
|
||||||
|
|
||||||
for _, a := range networkSettings.IPv4Addresses {
|
|
||||||
if a == address {
|
|
||||||
logger.Info("IPv4 address already exists: %s", address)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
networkSettings.IPv4Addresses = append(networkSettings.IPv4Addresses, address)
|
|
||||||
networkSettings.IPv4SubnetMasks = append(networkSettings.IPv4SubnetMasks, subnetMask)
|
|
||||||
incrementor++
|
|
||||||
logger.Info("Added IPv4 address: %s/%s", address, subnetMask)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveIPv4Address removes a previously added secondary IPv4 address.
|
|
||||||
func RemoveIPv4Address(address string) {
|
|
||||||
networkSettingsMutex.Lock()
|
|
||||||
defer networkSettingsMutex.Unlock()
|
|
||||||
|
|
||||||
for i, a := range networkSettings.IPv4Addresses {
|
|
||||||
if a == address {
|
|
||||||
networkSettings.IPv4Addresses = append(networkSettings.IPv4Addresses[:i], networkSettings.IPv4Addresses[i+1:]...)
|
|
||||||
networkSettings.IPv4SubnetMasks = append(networkSettings.IPv4SubnetMasks[:i], networkSettings.IPv4SubnetMasks[i+1:]...)
|
|
||||||
incrementor++
|
|
||||||
logger.Info("Removed IPv4 address: %s", address)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.Info("IPv4 address not found for removal: %s", address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetIPv4IncludedRoutes sets the included IPv4 routes
|
// SetIPv4IncludedRoutes sets the included IPv4 routes
|
||||||
func SetIPv4IncludedRoutes(routes []IPv4Route) {
|
func SetIPv4IncludedRoutes(routes []IPv4Route) {
|
||||||
networkSettingsMutex.Lock()
|
networkSettingsMutex.Lock()
|
||||||
|
|||||||
+127
-3
@@ -10,13 +10,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fosrl/newt/authdaemon"
|
"github.com/fosrl/newt/authdaemon"
|
||||||
"github.com/fosrl/newt/browsergateway"
|
"github.com/fosrl/newt/browsergateway"
|
||||||
"github.com/fosrl/newt/docker"
|
"github.com/fosrl/newt/docker"
|
||||||
"github.com/fosrl/newt/exitnode"
|
|
||||||
"github.com/fosrl/newt/healthcheck"
|
"github.com/fosrl/newt/healthcheck"
|
||||||
"github.com/fosrl/newt/internal/state"
|
"github.com/fosrl/newt/internal/state"
|
||||||
"github.com/fosrl/newt/internal/telemetry"
|
"github.com/fosrl/newt/internal/telemetry"
|
||||||
@@ -140,7 +140,129 @@ func (n *Newt) registerHandlers(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pingResults := exitnode.PingExitNodes(exitNodes, n.config.PreferEndpoint, n.connected)
|
if len(exitNodes) == 1 || n.config.PreferEndpoint != "" {
|
||||||
|
logger.Debug("Only one exit node available, using it directly: %s", exitNodes[0].Endpoint)
|
||||||
|
|
||||||
|
if n.config.PreferEndpoint != "" {
|
||||||
|
for _, node := range exitNodes {
|
||||||
|
if node.Endpoint == n.config.PreferEndpoint {
|
||||||
|
exitNodes[0] = node
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pingResults := []ExitNodePingResult{
|
||||||
|
{
|
||||||
|
ExitNodeID: exitNodes[0].ID,
|
||||||
|
LatencyMs: 0,
|
||||||
|
Weight: exitNodes[0].Weight,
|
||||||
|
Error: "",
|
||||||
|
Name: exitNodes[0].Name,
|
||||||
|
Endpoint: exitNodes[0].Endpoint,
|
||||||
|
WasPreviouslyConnected: exitNodes[0].WasPreviouslyConnected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
chainId := generateChainId()
|
||||||
|
n.pendingRegisterChainId = chainId
|
||||||
|
n.stopFunc = n.client.SendMessageInterval(topicWGRegister, map[string]interface{}{
|
||||||
|
"publicKey": n.publicKey.String(),
|
||||||
|
"pingResults": pingResults,
|
||||||
|
"newtVersion": n.config.Version,
|
||||||
|
"chainId": chainId,
|
||||||
|
}, 2*time.Second)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeResult struct {
|
||||||
|
Node ExitNode
|
||||||
|
Latency time.Duration
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]nodeResult, len(exitNodes))
|
||||||
|
const pingAttempts = 3
|
||||||
|
for i, node := range exitNodes {
|
||||||
|
var totalLatency time.Duration
|
||||||
|
var lastErr error
|
||||||
|
successes := 0
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
url := node.Endpoint
|
||||||
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
|
url = "http://" + url
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(url, "/ping") {
|
||||||
|
url = strings.TrimRight(url, "/") + "/ping"
|
||||||
|
}
|
||||||
|
for j := 0; j < pingAttempts; j++ {
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := httpClient.Get(url)
|
||||||
|
latency := time.Since(start)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
logger.Warn("Failed to ping exit node %d (%s) attempt %d: %v", node.ID, url, j+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
totalLatency += latency
|
||||||
|
successes++
|
||||||
|
}
|
||||||
|
var avgLatency time.Duration
|
||||||
|
if successes > 0 {
|
||||||
|
avgLatency = totalLatency / time.Duration(successes)
|
||||||
|
}
|
||||||
|
if successes == 0 {
|
||||||
|
results[i] = nodeResult{Node: node, Latency: 0, Err: lastErr}
|
||||||
|
} else {
|
||||||
|
results[i] = nodeResult{Node: node, Latency: avgLatency, Err: nil}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pingResults []ExitNodePingResult
|
||||||
|
for _, res := range results {
|
||||||
|
errMsg := ""
|
||||||
|
if res.Err != nil {
|
||||||
|
errMsg = res.Err.Error()
|
||||||
|
}
|
||||||
|
pingResults = append(pingResults, ExitNodePingResult{
|
||||||
|
ExitNodeID: res.Node.ID,
|
||||||
|
LatencyMs: res.Latency.Milliseconds(),
|
||||||
|
Weight: res.Node.Weight,
|
||||||
|
Error: errMsg,
|
||||||
|
Name: res.Node.Name,
|
||||||
|
Endpoint: res.Node.Endpoint,
|
||||||
|
WasPreviouslyConnected: res.Node.WasPreviouslyConnected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.connected {
|
||||||
|
var filteredPingResults []ExitNodePingResult
|
||||||
|
previouslyConnectedNodeIdx := -1
|
||||||
|
for i, res := range pingResults {
|
||||||
|
if res.WasPreviouslyConnected {
|
||||||
|
previouslyConnectedNodeIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goodNodeCount := 0
|
||||||
|
for i, res := range pingResults {
|
||||||
|
if i != previouslyConnectedNodeIdx && res.LatencyMs > 0 && res.Error == "" {
|
||||||
|
goodNodeCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if previouslyConnectedNodeIdx != -1 && goodNodeCount > 0 {
|
||||||
|
for i, res := range pingResults {
|
||||||
|
if i != previouslyConnectedNodeIdx {
|
||||||
|
filteredPingResults = append(filteredPingResults, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pingResults = filteredPingResults
|
||||||
|
logger.Info("Excluding previously connected exit node from ping results due to other available nodes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chainId := generateChainId()
|
chainId := generateChainId()
|
||||||
n.pendingRegisterChainId = chainId
|
n.pendingRegisterChainId = chainId
|
||||||
@@ -888,7 +1010,9 @@ func (n *Newt) registerHandlers(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bcChainId := generateChainId()
|
bcChainId := generateChainId()
|
||||||
n.pendingRegisterChainId = bcChainId
|
// Pangolin intentionally does not answer backwards-compatible
|
||||||
|
// registrations with newt/wg/connect. Do not replace the chain ID of
|
||||||
|
// the real registration while its response may already be in flight.
|
||||||
if err := n.client.SendMessage(topicWGRegister, map[string]interface{}{
|
if err := n.client.SendMessage(topicWGRegister, map[string]interface{}{
|
||||||
"publicKey": n.publicKey.String(),
|
"publicKey": n.publicKey.String(),
|
||||||
"newtVersion": n.config.Version,
|
"newtVersion": n.config.Version,
|
||||||
|
|||||||
+2
-1
@@ -280,7 +280,8 @@ func (n *Newt) startPingCheck(fn pingFunc, serverIP, tunnelID string) chan struc
|
|||||||
"chainId": pingChainId,
|
"chainId": pingChainId,
|
||||||
}, 3*time.Second)
|
}, 3*time.Second)
|
||||||
bcChainId := generateChainId()
|
bcChainId := generateChainId()
|
||||||
n.pendingRegisterChainId = bcChainId
|
// This compatibility message has no wg/connect response and must
|
||||||
|
// not supersede the pending real registration chain.
|
||||||
if err := n.client.SendMessage("newt/wg/register", map[string]interface{}{
|
if err := n.client.SendMessage("newt/wg/register", map[string]interface{}{
|
||||||
"publicKey": n.publicKey.String(),
|
"publicKey": n.publicKey.String(),
|
||||||
"backwardsCompatible": true,
|
"backwardsCompatible": true,
|
||||||
|
|||||||
+22
-9
@@ -2,7 +2,6 @@ package newt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
wgclients "github.com/fosrl/newt/clients"
|
wgclients "github.com/fosrl/newt/clients"
|
||||||
"github.com/fosrl/newt/exitnode"
|
|
||||||
"github.com/fosrl/newt/healthcheck"
|
"github.com/fosrl/newt/healthcheck"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,14 +35,28 @@ type TargetData struct {
|
|||||||
Targets []string `json:"targets"`
|
Targets []string `json:"targets"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExitNodeData, ExitNode and ExitNodePingResult are aliases for the shared
|
type ExitNodeData struct {
|
||||||
// exit-node ping dance types in package exitnode, kept here so existing code
|
ExitNodes []ExitNode `json:"exitNodes"`
|
||||||
// in this package can keep referring to them unqualified.
|
ChainId string `json:"chainId"`
|
||||||
type (
|
}
|
||||||
ExitNodeData = exitnode.ExitNodeData
|
|
||||||
ExitNode = exitnode.ExitNode
|
type ExitNode struct {
|
||||||
ExitNodePingResult = exitnode.ExitNodePingResult
|
ID int `json:"exitNodeId"`
|
||||||
)
|
Name string `json:"exitNodeName"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
WasPreviouslyConnected bool `json:"wasPreviouslyConnected"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExitNodePingResult struct {
|
||||||
|
ExitNodeID int `json:"exitNodeId"`
|
||||||
|
LatencyMs int64 `json:"latencyMs"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Name string `json:"exitNodeName"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
WasPreviouslyConnected bool `json:"wasPreviouslyConnected"`
|
||||||
|
}
|
||||||
|
|
||||||
type BlueprintResult struct {
|
type BlueprintResult struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ type versionResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ErrAutoUpdateUnsupportedInOfficialContainer indicates auto-update is not
|
// ErrAutoUpdateUnsupportedInOfficialContainer indicates auto-update is not
|
||||||
// available when running inside container images.
|
// available when running inside official Fossorial container images.
|
||||||
var ErrAutoUpdateUnsupportedInOfficialContainer = errors.New("auto-update unsupported in container images")
|
var ErrAutoUpdateUnsupportedInOfficialContainer = errors.New("auto-update unsupported in official Fossorial container images")
|
||||||
|
|
||||||
// isOfficialContainer returns true when the process is running inside an
|
// isOfficialContainer returns true when the process is running inside an
|
||||||
// official Fossorial-built container image. The image sets
|
// official Fossorial-built container image. The image sets
|
||||||
|
|||||||
+2
-20
@@ -28,15 +28,6 @@ import (
|
|||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
)
|
)
|
||||||
|
|
||||||
// writeDeadline bounds how long a websocket write may block before it is
|
|
||||||
// treated as a failure. Without this, a write to a TCP connection whose
|
|
||||||
// underlying network interface has disappeared (e.g. laptop sleep/resume,
|
|
||||||
// Wi-Fi roam) can sit buffered in the kernel for minutes without erroring.
|
|
||||||
// This matters even with the read-deadline/pong machinery below: if the
|
|
||||||
// WriteJSON call in sendPing blocks, execution never reaches the
|
|
||||||
// WriteControl ping that would otherwise trigger that read-side detection.
|
|
||||||
const writeDeadline = 10 * time.Second
|
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
config *Config
|
config *Config
|
||||||
@@ -266,9 +257,6 @@ func (c *Client) SendMessage(messageType string, data interface{}) error {
|
|||||||
|
|
||||||
c.writeMux.Lock()
|
c.writeMux.Lock()
|
||||||
defer c.writeMux.Unlock()
|
defer c.writeMux.Unlock()
|
||||||
if err := c.conn.SetWriteDeadline(time.Now().Add(writeDeadline)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.conn.WriteJSON(msg); err != nil {
|
if err := c.conn.WriteJSON(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -289,9 +277,6 @@ func (c *Client) SendMessageNoLog(messageType string, data interface{}) error {
|
|||||||
|
|
||||||
c.writeMux.Lock()
|
c.writeMux.Lock()
|
||||||
defer c.writeMux.Unlock()
|
defer c.writeMux.Unlock()
|
||||||
if err := c.conn.SetWriteDeadline(time.Now().Add(writeDeadline)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.conn.WriteJSON(msg); err != nil {
|
if err := c.conn.WriteJSON(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -775,17 +760,14 @@ func (c *Client) sendPing() {
|
|||||||
c.writeMux.Unlock()
|
c.writeMux.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := c.conn.SetWriteDeadline(time.Now().Add(writeDeadline))
|
err := c.conn.WriteJSON(pingMsg)
|
||||||
if err == nil {
|
|
||||||
err = c.conn.WriteJSON(pingMsg)
|
|
||||||
}
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
telemetry.IncWSMessage(c.metricsContext(), "out", "ping")
|
telemetry.IncWSMessage(c.metricsContext(), "out", "ping")
|
||||||
// Protocol-level ping: a standards-compliant server replies with a PONG,
|
// Protocol-level ping: a standards-compliant server replies with a PONG,
|
||||||
// which refreshes the read deadline. This is what lets us notice a
|
// which refreshes the read deadline. This is what lets us notice a
|
||||||
// half-open connection where writes still succeed (buffered) but the
|
// half-open connection where writes still succeed (buffered) but the
|
||||||
// peer is gone.
|
// peer is gone.
|
||||||
_ = c.conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeDeadline))
|
_ = c.conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(10*time.Second))
|
||||||
}
|
}
|
||||||
c.writeMux.Unlock()
|
c.writeMux.Unlock()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user