mirror of
https://github.com/fosrl/newt.git
synced 2026-07-23 08:01:29 +02:00
@@ -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
164
network/localendpoints.go
Normal 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
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
30
packages/arch/PKGBUILD
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user