Merge pull request #418 from fosrl/dev

1.15.0
This commit is contained in:
Owen Schwartz
2026-07-18 21:19:41 -04:00
committed by GitHub
8 changed files with 308 additions and 29 deletions

View File

@@ -504,9 +504,10 @@ func (s *WireGuardService) LoadRemoteConfig() error {
chainId := generateChainId()
s.pendingConfigChainId = chainId
s.stopGetConfig = s.client.SendMessageInterval("newt/wg/get-config", map[string]interface{}{
"publicKey": s.key.PublicKey().String(),
"port": s.Port,
"chainId": chainId,
"publicKey": s.key.PublicKey().String(),
"port": s.Port,
"chainId": chainId,
"localEndpoints": network.GetLocalEndpoints(s.Port, s.interfaceName),
}, 2*time.Second)
logger.Debug("Requesting WireGuard configuration from remote server")

164
network/localendpoints.go Normal file
View File

@@ -0,0 +1,164 @@
package network
import (
"net"
"regexp"
"sort"
"strconv"
"github.com/fosrl/newt/logger"
)
// Interface name patterns used to rank candidate local endpoints. Interfaces
// matching physicalInterfacePatterns are tried first, interfaces matching
// virtualInterfacePatterns (container/VPN/hypervisor bridges and the like)
// are tried last, and everything else falls in between.
var (
physicalInterfacePatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)^eth\d+$`),
regexp.MustCompile(`(?i)^en\d+$`),
regexp.MustCompile(`(?i)^eno\d+$`),
regexp.MustCompile(`(?i)^ens\d+$`),
regexp.MustCompile(`(?i)^enp\d+s\d+`),
regexp.MustCompile(`(?i)^wlan\d*$`),
regexp.MustCompile(`(?i)^wlp\d+s\d+`),
regexp.MustCompile(`(?i)^wl\d+$`),
regexp.MustCompile(`(?i)ethernet`),
regexp.MustCompile(`(?i)wi-?fi`),
regexp.MustCompile(`(?i)wireless`),
}
virtualInterfacePatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)docker`),
regexp.MustCompile(`(?i)podman`),
regexp.MustCompile(`(?i)^veth`),
regexp.MustCompile(`(?i)^virbr`),
regexp.MustCompile(`(?i)vmnet`),
regexp.MustCompile(`(?i)vboxnet`),
regexp.MustCompile(`(?i)virtualbox`),
regexp.MustCompile(`(?i)^vbox`),
regexp.MustCompile(`(?i)vmware`),
regexp.MustCompile(`(?i)hyper-?v`),
regexp.MustCompile(`(?i)vethernet`),
regexp.MustCompile(`(?i)npcap`),
regexp.MustCompile(`(?i)^tun\d*$`),
regexp.MustCompile(`(?i)^tap\d*$`),
regexp.MustCompile(`(?i)^wg\d*$`),
regexp.MustCompile(`(?i)^utun\d*$`),
regexp.MustCompile(`(?i)zerotier`),
regexp.MustCompile(`(?i)^zt`),
regexp.MustCompile(`(?i)tailscale`),
regexp.MustCompile(`(?i)^ppp\d*$`),
regexp.MustCompile(`(?i)bridge`),
regexp.MustCompile(`(?i)^br-`),
regexp.MustCompile(`(?i)^br\d+$`),
regexp.MustCompile(`(?i)^cni`),
regexp.MustCompile(`(?i)flannel`),
regexp.MustCompile(`(?i)weave`),
regexp.MustCompile(`(?i)kube`),
regexp.MustCompile(`(?i)isatap`),
regexp.MustCompile(`(?i)teredo`),
regexp.MustCompile(`(?i)bluetooth`),
regexp.MustCompile(`(?i)^awdl\d*$`),
regexp.MustCompile(`(?i)^llw\d*$`),
regexp.MustCompile(`(?i)p2p`),
}
)
const (
scorePhysical = 0
scoreUnknown = 10
scoreVirtual = 20
)
// interfaceScore ranks an interface name by how likely it is to be a
// real, usable network interface. Lower scores are tried first.
func interfaceScore(name string) int {
for _, re := range physicalInterfacePatterns {
if re.MatchString(name) {
return scorePhysical
}
}
for _, re := range virtualInterfacePatterns {
if re.MatchString(name) {
return scoreVirtual
}
}
return scoreUnknown
}
// GetLocalEndpoints returns "ip:port" strings (bracketed for IPv6, e.g.
// "[fe80::1]:51820") for every usable, non-loopback IP address bound to a
// network interface on this host. The list is ordered with interfaces most
// likely to be a genuine host network (wired/Wi-Fi) first, and interfaces
// that are typically synthetic (Docker, VPN tunnels, hypervisor bridges,
// etc.) last, so callers should try the results roughly in order.
//
// excludeInterface, if non-empty, is skipped entirely - this is normally the
// name of our own WireGuard/TUN interface, whose address is the tunnel IP
// and not a useful endpoint to advertise.
//
// If interfaces cannot be enumerated (e.g. insufficient OS permissions),
// an info message is logged and an empty slice is returned.
func GetLocalEndpoints(port uint16, excludeInterface string) []string {
ifaces, err := net.Interfaces()
if err != nil {
logger.Info("Unable to enumerate local network interfaces, localEndpoints will not be reported: %v", err)
return nil
}
type candidate struct {
score int
ip string
}
var candidates []candidate
for _, iface := range ifaces {
if excludeInterface != "" && iface.Name == excludeInterface {
continue
}
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
logger.Debug("Unable to read addresses for interface %s: %v", iface.Name, err)
continue
}
baseScore := interfaceScore(iface.Name)
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() || ip.IsUnspecified() {
continue
}
if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
// Link-local addresses (169.254.0.0/16, fe80::/10) aren't
// routable off the local segment, so they're never a
// reachable endpoint for a peer.
continue
}
candidates = append(candidates, candidate{score: baseScore, ip: ip.String()})
}
}
sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].score < candidates[j].score
})
portStr := strconv.Itoa(int(port))
endpoints := make([]string, 0, len(candidates))
for _, c := range candidates {
endpoints = append(endpoints, net.JoinHostPort(c.ip, portStr))
}
return endpoints
}

View File

@@ -11,6 +11,33 @@ import (
"github.com/vishvananda/netlink"
)
// VPNRouteMetric is the route metric/priority assigned to routes we add for
// the tunnel, so that an overlapping local/connected route is always
// preferred over the VPN route to the same destination rather than the two
// silently racing based on insertion order. It needs to be higher than any
// metric a local route is realistically going to have: on Linux, automatic
// metrics assigned by NetworkManager (which also apply to the connected
// subnet route, not just the default route) go up to 600 for Wi-Fi; on
// Windows, automatic interface metrics plus route metric rarely exceed a few
// hundred. 9999 comfortably clears both without needing to query the local
// routing table at add-time.
const VPNRouteMetric = 9999
// PreferLocalRoutes controls whether routes added by AddRoutes are given the
// explicit high VPNRouteMetric priority, so that an overlapping local/
// connected route always takes precedence over the VPN route to the same
// destination. Defaults to false (routes are added with the OS default
// metric/priority, matching behavior prior to the introduction of
// VPNRouteMetric); callers that want local routes to win opt in by setting
// this to true (e.g. from a config value) before routes are added.
var PreferLocalRoutes = false
// DarwinAddRoute adds a route via the BSD routing table. Unlike Linux/Windows,
// BSD's routing table has no per-route metric - preference between an
// overlapping local route and this VPN route is instead resolved by
// longest-prefix-match, and `route add` (as opposed to `route change`) fails
// rather than replacing an existing route to the same destination, so a local
// route is never displaced by one we add here.
func DarwinAddRoute(destination string, gateway string, interfaceName string) error {
if runtime.GOOS != "darwin" {
return nil
@@ -65,10 +92,16 @@ func LinuxAddRoute(destination string, gateway string, interfaceName string) err
return fmt.Errorf("invalid destination address: %v", err)
}
// Create route
// Create route. When PreferLocalRoutes is enabled, Priority is set
// explicitly (rather than left at the default of 0) so that this route
// never outranks a local/connected route to the same destination - see
// VPNRouteMetric.
route := &netlink.Route{
Dst: ipNet,
}
if PreferLocalRoutes {
route.Priority = VPNRouteMetric
}
if gateway != "" {
// Route with specific gateway
@@ -98,7 +131,7 @@ func LinuxAddRoute(destination string, gateway string, interfaceName string) err
return nil
}
func LinuxRemoveRoute(destination string) error {
func LinuxRemoveRoute(destination string, interfaceName string) error {
if runtime.GOOS != "linux" {
return nil
}
@@ -109,12 +142,26 @@ func LinuxRemoveRoute(destination string) error {
return fmt.Errorf("invalid destination address: %v", err)
}
// Create route to delete
// Create route to delete. LinkIndex and Priority are set to match the
// route we added exactly, so this only ever deletes the route we own -
// a local/native route to the same destination on a different
// interface (or with a different metric) must never be touched.
route := &netlink.Route{
Dst: ipNet,
}
if PreferLocalRoutes {
route.Priority = VPNRouteMetric
}
logger.Info("Removing route to %s", destination)
if interfaceName != "" {
link, err := netlink.LinkByName(interfaceName)
if err != nil {
return fmt.Errorf("failed to get interface %s: %v", interfaceName, err)
}
route.LinkIndex = link.Attrs().Index
}
logger.Info("Removing route to %s via interface %s", destination, interfaceName)
// Delete the route
if err := netlink.RouteDel(route); err != nil {
@@ -157,9 +204,9 @@ func RemoveRouteForServerIP(serverIP string, interfaceName string) error {
return DarwinRemoveRoute(serverIP)
}
// else if runtime.GOOS == "windows" {
// return WindowsRemoveRoute(serverIP)
// return WindowsRemoveRoute(serverIP, interfaceName)
// } else if runtime.GOOS == "linux" {
// return LinuxRemoveRoute(serverIP)
// return LinuxRemoveRoute(serverIP, interfaceName)
// }
return nil
}
@@ -242,8 +289,11 @@ func AddRoutes(remoteSubnets []string, interfaceName string) error {
return nil
}
// removeRoutesForRemoteSubnets removes routes for each subnet in RemoteSubnets
func RemoveRoutes(remoteSubnets []string) error {
// removeRoutesForRemoteSubnets removes routes for each subnet in RemoteSubnets.
// interfaceName must match the interface the routes were added on (see
// AddRoutes) so that only the routes we own are deleted, never an unrelated
// local/native route to the same destination on another interface.
func RemoveRoutes(remoteSubnets []string, interfaceName string) error {
if len(remoteSubnets) == 0 {
return nil
}
@@ -267,11 +317,11 @@ func RemoveRoutes(remoteSubnets []string) error {
logger.Error("Failed to remove Darwin route for subnet %s: %v", subnet, err)
}
case "windows":
if err := WindowsRemoveRoute(subnet); err != nil {
if err := WindowsRemoveRoute(subnet, interfaceName); err != nil {
logger.Error("Failed to remove Windows route for subnet %s: %v", subnet, err)
}
case "linux":
if err := LinuxRemoveRoute(subnet); err != nil {
if err := LinuxRemoveRoute(subnet, interfaceName); err != nil {
logger.Error("Failed to remove Linux route for subnet %s: %v", subnet, err)
}
case "android", "ios":

View File

@@ -6,6 +6,6 @@ func WindowsAddRoute(destination string, gateway string, interfaceName string) e
return nil
}
func WindowsRemoveRoute(destination string) error {
func WindowsRemoveRoute(destination string, interfaceName string) error {
return nil
}

View File

@@ -84,8 +84,15 @@ func WindowsAddRoute(destination string, gateway string, interfaceName string) e
return fmt.Errorf("either gateway or interface must be specified")
}
// Add the route using winipcfg
err = luid.AddRoute(prefix, nextHop, 1)
// Add the route using winipcfg. When PreferLocalRoutes is enabled,
// metric is set explicitly (rather than a low value like 1, which would
// nearly always outrank local routes) so that an overlapping local/
// connected route is preferred over this VPN route - see VPNRouteMetric.
var metric uint32
if PreferLocalRoutes {
metric = VPNRouteMetric
}
err = luid.AddRoute(prefix, nextHop, metric)
if err != nil {
return fmt.Errorf("failed to add route: %v", err)
}
@@ -93,7 +100,7 @@ func WindowsAddRoute(destination string, gateway string, interfaceName string) e
return nil
}
func WindowsRemoveRoute(destination string) error {
func WindowsRemoveRoute(destination string, interfaceName string) error {
// Parse destination CIDR
_, ipNet, err := net.ParseCIDR(destination)
if err != nil {
@@ -117,8 +124,25 @@ func WindowsRemoveRoute(destination string) error {
}
prefix := netip.PrefixFrom(addr, maskBits)
// Resolve the LUID of the interface we added the route on, so we only
// ever delete the route we own rather than any route matching the
// destination - a local/native route to the same destination on a
// different interface must never be touched.
var luid winipcfg.LUID
var haveLuid bool
if interfaceName != "" {
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)
}
haveLuid = true
}
// Get all routes and find the one to delete
// We need to get the LUID from the existing route
var family winipcfg.AddressFamily
if addr.Is4() {
family = 2 // AF_INET
@@ -131,17 +155,27 @@ func WindowsRemoveRoute(destination string) error {
return fmt.Errorf("failed to get route table: %v", err)
}
// Find and delete matching route
// Find and delete matching route. When we know which interface we added
// the route on, only delete the entry on that interface with the metric
// we added it with (see PreferLocalRoutes) so we never remove an
// unrelated local/native route to the same destination.
var wantMetric uint32
if PreferLocalRoutes {
wantMetric = VPNRouteMetric
}
for _, route := range routes {
routePrefix := route.DestinationPrefix.Prefix()
if routePrefix == prefix {
logger.Info("Removing route to %s", destination)
err = route.Delete()
if err != nil {
return fmt.Errorf("failed to delete route: %v", err)
}
return nil
if routePrefix != prefix {
continue
}
if haveLuid && (route.InterfaceLUID != luid || route.Metric != wantMetric) {
continue
}
logger.Info("Removing route to %s on interface %s", destination, interfaceName)
if err := route.Delete(); err != nil {
return fmt.Errorf("failed to delete route: %v", err)
}
return nil
}
return fmt.Errorf("route to %s not found", destination)

View File

@@ -430,7 +430,7 @@ func (n *Newt) registerHandlers(ctx context.Context) {
}
if n.config.UseNativeMainInterface {
if err := network.RemoveRoutes(data.Subnets); err != nil {
if err := network.RemoveRoutes(data.Subnets, n.config.NativeMainInterfaceName); err != nil {
logger.Warn("Failed to remove routes for subnets: %v", err)
}
}

View File

@@ -24,7 +24,7 @@ func (n *Newt) updateRemoteExitNodeSubnets(subnets []string) {
}
}
if len(toRemove) > 0 {
if err := network.RemoveRoutes(toRemove); err != nil {
if err := network.RemoveRoutes(toRemove, n.config.NativeMainInterfaceName); err != nil {
logger.Warn("Failed to remove old subnet routes: %v", err)
}
}
@@ -88,7 +88,7 @@ func (n *Newt) closeWgTunnel() {
}
toRemove = append(toRemove, n.activeRemoteSubnets...)
if len(toRemove) > 0 {
if err := network.RemoveRoutes(toRemove); err != nil {
if err := network.RemoveRoutes(toRemove, n.config.NativeMainInterfaceName); err != nil {
logger.Warn("Failed to remove native main tunnel routes: %v", err)
}
}

30
packages/arch/PKGBUILD Normal file
View File

@@ -0,0 +1,30 @@
# Maintainer: Fossorial <hello@fossorial.io>
pkgname=newt
pkgver=1.13.0
pkgrel=1
pkgdesc="Fully user space WireGuard tunnel client and TCP/UDP proxy for Pangolin"
arch=('x86_64' 'aarch64' 'armv7h' 'armv6h' 'riscv64')
url="https://github.com/fosrl/newt"
license=('AGPL3')
makedepends=('go')
source=("$pkgname-$pkgver.tar.gz::https://github.com/fosrl/newt/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP')
build() {
cd "$pkgname-$pkgver"
export CGO_ENABLED=0
export GOFLAGS="-trimpath -mod=readonly -modcacherw"
go build -ldflags "-X main.newtVersion=$pkgver -X main.newtPlatform=linux_$(go env GOARCH)" -o "$pkgname" .
}
check() {
cd "$pkgname-$pkgver"
go test ./... || true
}
package() {
cd "$pkgname-$pkgver"
install -Dm755 "$pkgname" "$pkgdir/usr/bin/$pkgname"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
}