From e1ed0c4b84c7248359dbdf4a40c7fad94748a599 Mon Sep 17 00:00:00 2001 From: Felicitas Pojtinger Date: Sun, 27 Aug 2023 21:07:31 +0200 Subject: [PATCH] fix: Reimplement `wrtcip` lifecycle to only start the TUN device once an IP is available to support Windows TUN drivers --- Hydrunfile | 2 +- README.md | 12 +-- cmd/weron/cmd/vpn_ethernet.go | 2 +- cmd/weron/cmd/vpn_ip.go | 7 +- pkg/wrtcip/netns_linux.go | 48 ++++------ pkg/wrtcip/netns_others.go | 53 +++++----- pkg/wrtcip/netns_windows.go | 46 +++++---- pkg/wrtcip/wrtcip.go | 175 +++++++++++++++++----------------- 8 files changed, 177 insertions(+), 168 deletions(-) diff --git a/Hydrunfile b/Hydrunfile index b0217c2..2846e9f 100755 --- a/Hydrunfile +++ b/Hydrunfile @@ -37,7 +37,7 @@ if [ "$1" = "go" ]; then make depend # Build - CGO_ENABLED=0 bagop -j "$(nproc)" -b "$2" -x '(android/*|ios/*|plan9/*|aix/*|linux/loong64|freebsd/riscv64)' -p "make build/$2 DST=\$DST" -d out + CGO_ENABLED=0 bagop -j "$(nproc)" -b "$2" -x '(android/*|ios/*|plan9/*|aix/*|linux/loong64|freebsd/riscv64|wasip1/wasm)' -p "make build/$2 DST=\$DST" -d out exit 0 fi diff --git a/README.md b/README.md index e2c84a3..e79fedf 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ For more information, see the [throughput measurement utility reference](#throug ### 6. Create a Layer 3 (IP) Overlay Network with `weron vpn ip` -If you want to join multiple nodes into an overlay network, the IP VPN is the best choice. It works similarly to i.e. Tailscale/WireGuard and can either dynamically allocate an IP address from a CIDR notation or statically assign one for you. On Windows, make sure to install [TAP-Windows](https://duckduckgo.com/?q=TAP-Windows&t=h_&ia=web) first. To get started, launch the VPN on the first peer: +If you want to join multiple nodes into an overlay network, the IP VPN is the best choice. It works similarly to i.e. Tailscale/WireGuard and can either dynamically allocate an IP address from a CIDR notation or statically assign one for you. On Windows, make sure to install [TAP-Windows](https://build.openvpn.net/downloads/releases/) first. Also note that due to technical limitations, only one IPv4 or IPv6 network and only one VPN instance at a time is supported on Windows; on macOS, only IPv6 networks are supported and IPv4 networks are ignored. To get started, launch the VPN on the first peer: ```shell $ sudo weron vpn ip --community mycommunity --password mypassword --key mykey --ips 2001:db8::1/64,192.0.2.1/24 @@ -580,16 +580,16 @@ Aliases: Flags: --community string ID of community to join - --dev string Name to give to the TAP device (i.e. weron0) (default is auto-generated; only supported on Linux, macOS and Windows) + --dev string Name to give to the TUN device (i.e. weron0) (default is auto-generated; only supported on Linux) --force-relay Force usage of TURN servers -h, --help help for ip --ice strings Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp) (default [stun:stun.l.google.com:19302]) --id-channel string Channel to use to negotiate names (default "weron/ip/id") - --ips strings Comma-separated list of IP networks to claim an IP address from and and give to the TUN device (i.e. 2001:db8::1/32,192.0.2.1/24) (on Windows, only one IPv4 and one IPv6 address are supported; on macOS, IPv4 addresses are ignored) + --ips strings Comma-separated list of IP networks to claim an IP address from and and give to the TUN device (i.e. 2001:db8::1/32,192.0.2.1/24) (on Windows, only one IP network (either IPv4 or IPv6) is supported; on macOS, IPv4 networks are ignored) --key string Encryption key for community --kicks duration Time to wait for kicks (default 5s) --max-retries int Maximum amount of times to try and claim an IP address (default 200) - --parallel int Amount of threads to use to decode frames (default 8) + --parallel int Amount of threads to use to decode frames (default 20) --password string Password for community --raddr string Remote address (default "wss://weron.up.railway.app/") --static Try to claim the exact IPs specified in the --ips flag statically instead of selecting a random one from the specified network @@ -613,13 +613,13 @@ Aliases: Flags: --community string ID of community to join - --dev string Name to give to the TAP device (i.e. weron0) (default is auto-generated; only supported on Linux, macOS and Windows) + --dev string Name to give to the TAP device (i.e. weron0) (default is auto-generated; only supported on Linux and macOS) --force-relay Force usage of TURN servers -h, --help help for ethernet --ice strings Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp) (default [stun:stun.l.google.com:19302]) --key string Encryption key for community --mac string MAC address to give to the TAP device (i.e. 3a:f8:de:7b:ef:52) (default is auto-generated; only supported on Linux) - --parallel int Amount of threads to use to decode frames (default 8) + --parallel int Amount of threads to use to decode frames (default 20) --password string Password for community --raddr string Remote address (default "wss://weron.up.railway.app/") --timeout duration Time to wait for connections (default 10s) diff --git a/cmd/weron/cmd/vpn_ethernet.go b/cmd/weron/cmd/vpn_ethernet.go index f4bca51..75050ff 100644 --- a/cmd/weron/cmd/vpn_ethernet.go +++ b/cmd/weron/cmd/vpn_ethernet.go @@ -107,7 +107,7 @@ func init() { vpnEthernetCmd.PersistentFlags().String(keyFlag, "", "Encryption key for community") vpnEthernetCmd.PersistentFlags().StringSlice(iceFlag, []string{"stun:stun.l.google.com:19302"}, "Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp)") vpnEthernetCmd.PersistentFlags().Bool(forceRelayFlag, false, "Force usage of TURN servers") - vpnEthernetCmd.PersistentFlags().String(devFlag, "", "Name to give to the TAP device (i.e. weron0) (default is auto-generated; only supported on Linux, macOS and Windows)") + vpnEthernetCmd.PersistentFlags().String(devFlag, "", "Name to give to the TAP device (i.e. weron0) (default is auto-generated; only supported on Linux and macOS)") vpnEthernetCmd.PersistentFlags().String(macFlag, "", "MAC address to give to the TAP device (i.e. 3a:f8:de:7b:ef:52) (default is auto-generated; only supported on Linux)") vpnEthernetCmd.PersistentFlags().Int(parallelFlag, runtime.NumCPU(), "Amount of threads to use to decode frames") diff --git a/cmd/weron/cmd/vpn_ip.go b/cmd/weron/cmd/vpn_ip.go index 5fc76ea..bae4798 100644 --- a/cmd/weron/cmd/vpn_ip.go +++ b/cmd/weron/cmd/vpn_ip.go @@ -19,7 +19,6 @@ import ( ) var ( - errMissingIPs = errors.New("no IP(s) provided") errInvalidCIDR = errors.New("invalid CIDR notation for IPs") ) @@ -54,7 +53,7 @@ var vpnIPCmd = &cobra.Command{ } if len(viper.GetStringSlice(ipsFlag)) <= 0 { - return errMissingIPs + return wrtcip.ErrMissingIPs } for _, ip := range viper.GetStringSlice(ipsFlag) { @@ -131,8 +130,8 @@ func init() { vpnIPCmd.PersistentFlags().String(keyFlag, "", "Encryption key for community") vpnIPCmd.PersistentFlags().StringSlice(iceFlag, []string{"stun:stun.l.google.com:19302"}, "Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp)") vpnIPCmd.PersistentFlags().Bool(forceRelayFlag, false, "Force usage of TURN servers") - vpnIPCmd.PersistentFlags().String(devFlag, "", "Name to give to the TAP device (i.e. weron0) (default is auto-generated; only supported on Linux, macOS and Windows)") - vpnIPCmd.PersistentFlags().StringSlice(ipsFlag, []string{""}, "Comma-separated list of IP networks to claim an IP address from and and give to the TUN device (i.e. 2001:db8::1/32,192.0.2.1/24) (on Windows, only one IPv4 and one IPv6 address are supported; on macOS, IPv4 addresses are ignored)") + vpnIPCmd.PersistentFlags().String(devFlag, "", "Name to give to the TUN device (i.e. weron0) (default is auto-generated; only supported on Linux)") + vpnIPCmd.PersistentFlags().StringSlice(ipsFlag, []string{""}, "Comma-separated list of IP networks to claim an IP address from and and give to the TUN device (i.e. 2001:db8::1/32,192.0.2.1/24) (on Windows, only one IP network (either IPv4 or IPv6) is supported; on macOS, IPv4 networks are ignored)") vpnIPCmd.PersistentFlags().Bool(staticFlag, false, "Try to claim the exact IPs specified in the --"+ipsFlag+" flag statically instead of selecting a random one from the specified network") vpnIPCmd.PersistentFlags().Int(parallelFlag, runtime.NumCPU(), "Amount of threads to use to decode frames") vpnIPCmd.PersistentFlags().String(idChannelFlag, services.IPID, "Channel to use to negotiate names") diff --git a/pkg/wrtcip/netns_linux.go b/pkg/wrtcip/netns_linux.go index e87a66a..5989e04 100644 --- a/pkg/wrtcip/netns_linux.go +++ b/pkg/wrtcip/netns_linux.go @@ -1,46 +1,40 @@ package wrtcip import ( - "net" - "github.com/songgao/water" "github.com/vishvananda/netlink" ) -func getPlatformSpecificParams(name string) water.PlatformSpecificParams { - return water.PlatformSpecificParams{ - Name: name, - } -} - -func setIPAddress(linkName string, ipaddr string, ipv4 bool) error { - link, err := netlink.LinkByName(linkName) +func setupTUN(name string, ips []string) (*water.Interface, int, error) { + tun, err := water.New(water.Config{ + DeviceType: water.TUN, + PlatformSpecificParams: water.PlatformSpecificParams{ + Name: name, + }, + }) if err != nil { - return err + return nil, 0, err } - ip, err := netlink.ParseAddr(ipaddr) + link, err := netlink.LinkByName(tun.Name()) if err != nil { - return err + return tun, 0, err } - return netlink.AddrAdd(link, ip) -} + for _, rawIP := range ips { + ip, err := netlink.ParseAddr(rawIP) + if err != nil { + return tun, 0, err + } -func getMTU(linkName string) (int, error) { - iface, err := net.InterfaceByName(linkName) - if err != nil { - return -1, err + if err := netlink.AddrAdd(link, ip); err != nil { + return tun, 0, err + } } - return iface.MTU, nil -} - -func setLinkUp(linkName string) error { - link, err := netlink.LinkByName(linkName) - if err != nil { - return err + if err := netlink.LinkSetUp(link); err != nil { + return tun, 0, err } - return netlink.LinkSetUp(link) + return tun, link.Attrs().MTU, nil } diff --git a/pkg/wrtcip/netns_others.go b/pkg/wrtcip/netns_others.go index 46e46b1..01c19a8 100644 --- a/pkg/wrtcip/netns_others.go +++ b/pkg/wrtcip/netns_others.go @@ -7,39 +7,48 @@ import ( "fmt" "net" "os/exec" + "runtime" "github.com/songgao/water" ) -func getPlatformSpecificParams(name string) water.PlatformSpecificParams { - return water.PlatformSpecificParams{} -} +func setupTUN(name string, ips []string) (*water.Interface, int, error) { + tun, err := water.New(water.Config{ + DeviceType: water.TUN, + PlatformSpecificParams: water.PlatformSpecificParams{}, + }) + if err != nil { + return nil, 0, err + } -func setIPAddress(linkName string, ipaddr string, ipv4 bool) error { - if ipv4 { - output, err := exec.Command("ifconfig", linkName, "inet", ipaddr).CombinedOutput() + for _, rawIP := range ips { + ip, _, err := net.ParseCIDR(rawIP) if err != nil { - return fmt.Errorf("could not add IPv4 address to interface: %v: %v", string(output), err) + return tun, 0, err } - } else { - output, err := exec.Command("ifconfig", linkName, "inet6", "add", ipaddr).CombinedOutput() - if err != nil { - return fmt.Errorf("could not add IPv6 address to interface: %v: %v", string(output), err) + + if ip.To4() != nil { + // macOS does not support IPv4 TUN + if runtime.GOOS == "darwin" && ip.To4() != nil { + continue + } + + output, err := exec.Command("ifconfig", tun.Name(), "inet", rawIP).CombinedOutput() + if err != nil { + return tun, 0, fmt.Errorf("could not add IPv4 address to interface: %v: %v", string(output), err) + } + } else { + output, err := exec.Command("ifconfig", tun.Name(), "inet6", "add", rawIP).CombinedOutput() + if err != nil { + return tun, 0, fmt.Errorf("could not add IPv6 address to interface: %v: %v", string(output), err) + } } } - return nil -} - -func getMTU(linkName string) (int, error) { - iface, err := net.InterfaceByName(linkName) + iface, err := net.InterfaceByName(tun.Name()) if err != nil { - return -1, err + return tun, 0, err } - return iface.MTU, nil -} - -func setLinkUp(linkName string) error { - return nil + return tun, iface.MTU, nil } diff --git a/pkg/wrtcip/netns_windows.go b/pkg/wrtcip/netns_windows.go index 63ffff0..f0005ff 100644 --- a/pkg/wrtcip/netns_windows.go +++ b/pkg/wrtcip/netns_windows.go @@ -3,40 +3,44 @@ package wrtcip import ( "fmt" "net" + "os/exec" "github.com/songgao/water" - "os/exec" ) -func getPlatformSpecificParams(name string) water.PlatformSpecificParams { - return water.PlatformSpecificParams{} -} +func setupTUN(name string, ips []string) (*water.Interface, int, error) { + tun, err := water.New(water.Config{ + DeviceType: water.TUN, + PlatformSpecificParams: water.PlatformSpecificParams{ + ComponentID: "tap0901", + Network: ips[0], + }, + }) + if err != nil { + return nil, 0, err + } + + ip, _, err := net.ParseCIDR(ips[0]) + if err != nil { + return tun, 0, err + } -func setIPAddress(linkName string, ipaddr string, ipv4 bool) error { - if ipv4 { - output, err := exec.Command("netsh", "interface", "ipv4", "set", "address", linkName, "static", ipaddr).CombinedOutput() + if ip.To4() != nil { + output, err := exec.Command("netsh", "interface", "ipv4", "set", "address", tun.Name(), "static", ips[0]).CombinedOutput() if err != nil { - return fmt.Errorf("could not add IPv4 address to interface: %v: %v", string(output), err) + return tun, 0, fmt.Errorf("could not add IPv4 address to interface: %v: %v", string(output), err) } } else { - output, err := exec.Command("netsh", "interface", "ipv6", "set", "address", linkName, ipaddr).CombinedOutput() + output, err := exec.Command("netsh", "interface", "ipv6", "set", "address", tun.Name(), ips[0]).CombinedOutput() if err != nil { - return fmt.Errorf("could not add IPv6 address to interface: %v: %v", string(output), err) + return tun, 0, fmt.Errorf("could not add IPv6 address to interface: %v: %v", string(output), err) } } - return nil -} - -func getMTU(linkName string) (int, error) { - iface, err := net.InterfaceByName(linkName) + iface, err := net.InterfaceByName(tun.Name()) if err != nil { - return -1, err + return tun, 0, err } - return iface.MTU, nil -} - -func setLinkUp(linkName string) error { - return nil + return tun, iface.MTU, nil } diff --git a/pkg/wrtcip/wrtcip.go b/pkg/wrtcip/wrtcip.go index 0e2e82f..4bef42e 100644 --- a/pkg/wrtcip/wrtcip.go +++ b/pkg/wrtcip/wrtcip.go @@ -3,6 +3,7 @@ package wrtcip import ( "context" "encoding/binary" + "errors" "fmt" "net" "net/netip" @@ -27,6 +28,8 @@ const ( var ( json = jsoniter.ConfigCompatibleWithStandardLibrary + + ErrMissingIPs = errors.New("no IPs provided") ) // AdapterConfig configures the adapter @@ -53,8 +56,10 @@ type Adapter struct { cancel context.CancelFunc adapter *wrtcconn.NamedAdapter tun *water.Interface - mtu int ids chan string + + mtu int + mtuCond *sync.Cond } type peerWithIP struct { @@ -90,22 +95,15 @@ func NewAdapter( cancel: cancel, ids: make(chan string), + + mtuCond: sync.NewCond(&sync.Mutex{}), } } -// Open connects the adapter to the signaler and creates the TUN device +// Open connects the adapter to the signaler func (a *Adapter) Open() error { log.Trace().Msg("Opening adapter") - var err error - a.tun, err = water.New(water.Config{ - DeviceType: water.TUN, - PlatformSpecificParams: getPlatformSpecificParams(a.config.Device), - }) - if err != nil { - return err - } - for _, rawIP := range a.config.CIDRs { ip, _, err := net.ParseCIDR(rawIP) if err != nil { @@ -213,13 +211,12 @@ func (a *Adapter) Open() error { a.ctx, ) + var err error a.ids, err = a.adapter.Open() if err != nil { return err } - a.mtu, err = getMTU(a.tun.Name()) - return err } @@ -227,8 +224,10 @@ func (a *Adapter) Open() error { func (a *Adapter) Close() error { log.Trace().Msg("Closing adapter") - if err := a.tun.Close(); err != nil { - return err + if a.tun != nil { + if err := a.tun.Close(); err != nil { + return err + } } return a.adapter.Close() @@ -239,61 +238,6 @@ func (a *Adapter) Wait() error { peers := map[string]*peerWithIP{} var peersLock sync.Mutex - go func() { - sem := semaphore.NewWeighted(int64(a.config.Parallel)) - - for { - buf := make([]byte, a.mtu+headerLength) - - if _, err := a.tun.Read(buf); err != nil { - log.Debug().Err(err).Msg("Could not read from TUN device, continuing") - - continue - } - - go func() { - if err := sem.Acquire(a.ctx, 1); err != nil { - log.Debug().Err(err).Msg("Could not acquire semaphore, stopping") - - return - } - defer sem.Release(1) - - var dst net.IP - var packet layers.IPv4 - if err := packet.DecodeFromBytes(buf, gopacket.NilDecodeFeedback); err != nil { - var packet layers.IPv6 - if err := packet.DecodeFromBytes(buf, gopacket.NilDecodeFeedback); err != nil { - log.Debug().Err(err).Msg("Could not unmarshal packet, stopping") - - return - } else { - dst = packet.DstIP - } - } else { - dst = packet.DstIP - } - - peersLock.Lock() - for _, peer := range peers { - // Send if matching destination, multicast or broadcast IP - if dst.Equal(peer.ip) || ((dst.IsMulticast() || dst.IsInterfaceLocalMulticast() || dst.IsInterfaceLocalMulticast()) && len(dst) == len(peer.ip)) || (peer.ip.To4() != nil && dst.Equal(getBroadcastAddr(peer.net))) { - if _, err := peer.Conn.Write(buf); err != nil { - log.Debug(). - Err(err). - Str("channelID", peer.ChannelID). - Str("peerID", peer.PeerID). - Msg("Could not write to peer, continuing") - - continue - } - } - } - peersLock.Unlock() - }() - } - }() - for { select { case <-a.ctx.Done(): @@ -318,27 +262,81 @@ func (a *Adapter) Wait() error { return err } - for _, rawIP := range ips { - ip, _, err := net.ParseCIDR(rawIP) - if err != nil { - log.Debug().Err(err).Msg("Could not parse IP address, continuing") - - continue - } - - // macOS does not support IPv4 TUN - if runtime.GOOS == "darwin" && ip.To4() != nil { - continue - } + if len(ips) <= 0 { + return ErrMissingIPs + } - if err = setIPAddress(a.tun.Name(), rawIP, ip.To4() != nil); err != nil { - return err - } + // Close old TUN device if it isn't already closed + if a.tun != nil { + _ = a.tun.Close() } - if err := setLinkUp(a.tun.Name()); err != nil { + a.mtuCond.L.Lock() + var err error + a.tun, a.mtu, err = setupTUN(a.config.Device, ips) + if err != nil { + a.mtuCond.L.Unlock() + return err } + // Signal that the MTU is available/the TUN device is started + a.mtuCond.Broadcast() + a.mtuCond.L.Unlock() + + go func() { + sem := semaphore.NewWeighted(int64(a.config.Parallel)) + + for { + buf := make([]byte, a.mtu+headerLength) // No need for the MTU cond here since its guaranteed to be set + + if _, err := a.tun.Read(buf); err != nil { + log.Debug().Err(err).Msg("Could not read from TUN device, returning") + + return + } + + go func() { + if err := sem.Acquire(a.ctx, 1); err != nil { + log.Debug().Err(err).Msg("Could not acquire semaphore, stopping") + + return + } + defer sem.Release(1) + + var dst net.IP + var packet layers.IPv4 + if err := packet.DecodeFromBytes(buf, gopacket.NilDecodeFeedback); err != nil { + var packet layers.IPv6 + if err := packet.DecodeFromBytes(buf, gopacket.NilDecodeFeedback); err != nil { + log.Debug().Err(err).Msg("Could not unmarshal packet, stopping") + + return + } else { + dst = packet.DstIP + } + } else { + dst = packet.DstIP + } + + peersLock.Lock() + for _, peer := range peers { + // Send if matching destination, multicast or broadcast IP + if dst.Equal(peer.ip) || ((dst.IsMulticast() || dst.IsInterfaceLocalMulticast() || dst.IsInterfaceLocalMulticast()) && len(dst) == len(peer.ip)) || (peer.ip.To4() != nil && dst.Equal(getBroadcastAddr(peer.net))) { + if _, err := peer.Conn.Write(buf); err != nil { + log.Debug(). + Err(err). + Str("channelID", peer.ChannelID). + Str("peerID", peer.PeerID). + Msg("Could not write to peer, continuing") + + continue + } + } + } + peersLock.Unlock() + }() + } + }() case peer := <-a.adapter.Accept(): log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Connected to peer") @@ -401,7 +399,12 @@ func (a *Adapter) Wait() error { } for { + a.mtuCond.L.Lock() + if a.mtu <= 0 { + a.mtuCond.Wait() + } buf := make([]byte, a.mtu+headerLength) + a.mtuCond.L.Unlock() if _, err := peer.Conn.Read(buf); err != nil { log.Debug().