diff --git a/net/addrs/common.go b/net/addrs/common.go index f52b619b9..48ac7da19 100644 --- a/net/addrs/common.go +++ b/net/addrs/common.go @@ -1,16 +1,22 @@ package addrs import ( + "cmp" + "fmt" "net" "regexp" + "runtime" "strconv" + "strings" - "github.com/ethereum/go-ethereum/log" "golang.org/x/exp/slices" + "github.com/anyproto/anytype-heart/pkg/lib/logging" "github.com/anyproto/anytype-heart/util/slice" ) +var log = logging.Logger("anytype-net") + type Interface struct { net.Interface Addrs []InterfaceAddr @@ -55,7 +61,7 @@ func (i NetInterfaceWithAddrCache) GetAddr() []net.Addr { if i.cachedErr != nil { return nil } - i.cachedAddrs, i.cachedErr = i.Addrs() + i.cachedAddrs, i.cachedErr = i.Interface.Addrs() if i.cachedErr != nil { log.Warn("interface GetAddr error: %v", i.cachedErr) } @@ -80,35 +86,110 @@ func (i InterfacesAddrs) Equal(other InterfacesAddrs) bool { } myStr := getStrings(i) otherStr := getStrings(other) - return slices.Equal(myStr, otherStr) + // compare slices without order + if !slices.Equal(myStr, otherStr) { + log.Debug(fmt.Sprintf("addrs compare: strings mismatch: %v != %v", myStr, otherStr)) + return false + } + return true } var ( - ifaceRe = regexp.MustCompile(`^([a-z]*?)([0-9]+)$`) + ifaceRe = regexp.MustCompile(`^([a-z]*?)([0-9]+)$`) + ifaceWindowsRe = regexp.MustCompile(`^(.*?)([0-9]*)$`) + // ifaceReBusSlot used for prefixBusSlot naming schema used in newer linux distros https://cgit.freedesktop.org/systemd/systemd/tree/src/udev/udev-builtin-net_id.c#n20 - ifaceReBusSlot = regexp.MustCompile(`^([a-z]*?)p([0-9]+)s([0-9a-f]+)$`) + ifaceReBusSlot = regexp.MustCompile(`^(?Penp|eno|ens|enx|wlp|wlx)(?P[0-9a-fA-F]*)s?(?P[0-9a-fA-F]*)?$`) ) -func parseInterfaceName(name string) (prefix string, bus int, num int64) { +func cleanInterfaceName(name string) (clean string, namingType NamingType) { + if strings.HasPrefix(name, "en") || + strings.HasPrefix(name, "wl") || + strings.HasPrefix(name, "eth") { + + lastSymbol := name[len(name)-1] + switch NamingType(lastSymbol) { + case NamingTypeBusSlot, NamingTypeHotplug, NamingTypeMac, NamingTypeOnboard: + return name[0 : len(name)-1], NamingType(lastSymbol) + } + } + + return name, NamingTypeOld +} + +type NamingType string + +const ( + NamingTypeOld NamingType = "" + NamingTypeOnboard NamingType = "o" + NamingTypeBusSlot NamingType = "p" + NamingTypeMac NamingType = "x" + NamingTypeHotplug NamingType = "s" +) + +func (n NamingType) Priority() int { + switch n { + case NamingTypeOld: + return 0 + case NamingTypeOnboard: + return 1 + case NamingTypeBusSlot: + return 2 + case NamingTypeMac: + return 3 + case NamingTypeHotplug: + return 4 + default: + return 5 + } +} + +// parseInterfaceName parses interface name and returns prefix, naming type, bus number and slot number +// e.g. enp0s3 -> en, NamingTypeBusSlot, 0, 3 +// bus and slot are interpreted as hex numbers +// bus is also used for mac address +// in case of enx001122334455 -> en, NamingTypeMac, 0x001122334455, 0 +func parseInterfaceName(name string) (iface string, namingType NamingType, busNum int64, num int64) { + if runtime.GOOS == "windows" { + name, num = parseInterfaceWindowsName(name) + return + } // try new-style naming schema first (enp0s3, wlp2s0, ...) res := ifaceReBusSlot.FindStringSubmatch(name) if len(res) > 0 { - if len(res) > 1 { - prefix = res[1] - } - if len(res) > 2 { - bus, _ = strconv.Atoi(res[2]) - } - if len(res) > 3 { - numHex := res[3] - num, _ = strconv.ParseInt(numHex, 16, 32) + + for i, subName := range ifaceReBusSlot.SubexpNames() { + if i > 0 && res[i] != "" { + switch subName { + case "type": + iface, namingType = cleanInterfaceName(res[i]) + case "bus": + busNum, _ = strconv.ParseInt(res[i], 16, 64) + case "slot": // or mac + num, _ = strconv.ParseInt(res[i], 16, 64) + } + } } return } // try old-style naming schema (eth0, wlan0, ...) res = ifaceRe.FindStringSubmatch(name) if len(res) > 1 { - prefix = res[1] + iface = res[1] + } + if len(res) > 2 { + num, _ = strconv.ParseInt(res[2], 10, 32) + } + if iface == "" { + + } + return +} + +func parseInterfaceWindowsName(name string) (iface string, num int64) { + res := ifaceWindowsRe.FindStringSubmatch(name) + if len(res) > 1 { + iface = res[1] } if len(res) > 2 { num, _ = strconv.ParseInt(res[2], 10, 32) @@ -116,42 +197,54 @@ func parseInterfaceName(name string) (prefix string, bus int, num int64) { return } -func (i InterfacesAddrs) SortWithPriority(priority []string) { - less := func(a, b NetInterfaceWithAddrCache) bool { - aPrefix, aBus, aNum := parseInterfaceName(a.Name) - bPrefix, bBus, bNum := parseInterfaceName(b.Name) +type interfaceComparer struct { + priority []string +} - aPrioirity := slice.FindPos(priority, aPrefix) - bPrioirity := slice.FindPos(priority, bPrefix) +func (i interfaceComparer) Compare(a, b string) int { + aPrefix, aType, aBus, aNum := parseInterfaceName(a) + bPrefix, bType, bBus, bNum := parseInterfaceName(b) - if aPrefix == bPrefix { - return aNum < bNum - } else if aPrioirity == -1 && bPrioirity == -1 { - // sort alphabetically - return aPrefix < bPrefix - } else if aPrioirity != -1 && bPrioirity != -1 { - // in case we have [eth, wlan] - if aPrioirity == bPrioirity { - // prioritize eth0 over wlan0 - return aPrioirity < bPrioirity + aPrioirity := slice.FindPos(i.priority, aPrefix) + bPrioirity := slice.FindPos(i.priority, bPrefix) + + if aPrioirity != -1 && bPrioirity != -1 || aPrioirity == -1 && bPrioirity == -1 { + if aPrefix != bPrefix { + if aPrioirity != -1 && bPrioirity != -1 { + // prioritize by priority + return cmp.Compare(aPrioirity, bPrioirity) + } else { + // prioritize by prefix + return cmp.Compare(aPrefix, bPrefix) } - // prioritise wlan1 over eth8 - if aBus != bBus { - return aBus < bBus - } - return aNum < bNum - } else if aPrioirity != -1 { - return true - } else { - return false } + if aType != bType { + return cmp.Compare(aType.Priority(), bType.Priority()) + } + if aBus != bBus { + return cmp.Compare(aBus, bBus) + } + if aNum != bNum { + return cmp.Compare(aNum, bNum) + } + // shouldn't be a case + return cmp.Compare(a, b) } - slices.SortFunc(i.Interfaces, func(a, b NetInterfaceWithAddrCache) int { - if less(a, b) { - return -1 - } + + if aPrioirity == -1 { return 1 - }) + } else { + return -1 + } +} + +func (i InterfacesAddrs) SortInterfacesWithPriority(priority []string) { + sorter := interfaceComparer{priority: priority} + + compare := func(a, b NetInterfaceWithAddrCache) int { + return sorter.Compare(a.Name, b.Name) + } + slices.SortFunc(i.Interfaces, compare) } func (i InterfacesAddrs) NetInterfaces() []net.Interface { @@ -223,3 +316,14 @@ func (i InterfacesAddrs) findInterfacePosByIP(ip net.IP) (pos int, equal bool) { } return -1, false } + +func filterInterfaces(ifaces []NetInterfaceWithAddrCache) []NetInterfaceWithAddrCache { + return slice.Filter(ifaces, func(iface NetInterfaceWithAddrCache) bool { + if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagMulticast != 0 && iface.Flags&net.FlagLoopback == 0 { + if len(iface.GetAddr()) > 0 { + return true + } + } + return false + }) +} diff --git a/net/addrs/common_test.go b/net/addrs/common_test.go index 8d916c276..6ed49705a 100644 --- a/net/addrs/common_test.go +++ b/net/addrs/common_test.go @@ -1,6 +1,11 @@ package addrs -import "testing" +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) func Test_parseInterfaceName(t *testing.T) { type args struct { @@ -10,18 +15,27 @@ func Test_parseInterfaceName(t *testing.T) { name string args args wantPrefix string - wantBus int + wantType NamingType + wantBus int64 wantNum int64 }{ - {"eth0", args{"eth0"}, "eth", 0, 0}, - {"eth1", args{"eth1"}, "eth", 0, 1}, - {"eth10", args{"eth10"}, "eth", 0, 10}, - {"enp0s10", args{"enp0s10"}, "en", 0, 16}, - {"wlp0s20f3", args{"wlp0s20f3"}, "wl", 0, 8435}, + {"eth0", args{"eth0"}, "eth", NamingTypeOld, 0, 0}, + {"eth1", args{"eth1"}, "eth", NamingTypeOld, 0, 1}, + {"eth10", args{"eth10"}, "eth", NamingTypeOld, 0, 10}, + {"enp0s10", args{"enp0s10"}, "en", NamingTypeBusSlot, 0, 0x10}, + {"wlp0s20f3", args{"wlp0s20f3"}, "wl", NamingTypeBusSlot, 0, 0x20f3}, + {"tun0", args{"tun0"}, "tun", NamingTypeOld, 0, 0}, + {"tap0", args{"tap0"}, "tap", NamingTypeOld, 0, 0}, + {"lo0", args{"lo0"}, "lo", NamingTypeOld, 0, 0}, + {"lo1", args{"lo1"}, "lo", NamingTypeOld, 0, 1}, + {"lo10", args{"lo10"}, "lo", NamingTypeOld, 0, 10}, + {"wlx001122334455", args{"wlx001122334455"}, "wl", NamingTypeMac, 0x001122334455, 0}, + {"wlxffffffffffff", args{"wlxffffffffffff"}, "wl", NamingTypeMac, 0xffffffffffff, 0}, + {"eno16777736", args{"eno16777736"}, "en", NamingTypeOnboard, 0x16777736, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotPrefix, gotBus, gotNum := parseInterfaceName(tt.args.name) + gotPrefix, gotType, gotBus, gotNum := parseInterfaceName(tt.args.name) if gotPrefix != tt.wantPrefix { t.Errorf("parseInterfaceName() gotPrefix = %v, want %v", gotPrefix, tt.wantPrefix) } @@ -31,6 +45,54 @@ func Test_parseInterfaceName(t *testing.T) { if gotNum != tt.wantNum { t.Errorf("parseInterfaceName() gotNum = %v, want %v", gotNum, tt.wantNum) } + if gotType != tt.wantType { + t.Errorf("parseInterfaceName() gotType = %v, want %v", gotType, tt.wantType) + } }) } } + +// TestInterfaceSorterSorting tests sorting a list of interface names using the Compare method. +func TestInterfaceSorterSorting(t *testing.T) { + sorter := interfaceComparer{ + priority: []string{"wl", "wlan", "en", "eth", "tun", "tap", "utun"}, + } + + // List of interfaces to sort + interfaces := []string{ + "tap0", + "awdl0", + "eth0", + "eno1", + "wlp2s0", + "ens2", + "enp0s3", + "tun0", + "wlan0", + "enx001122334455", + "wlx001122334455", + } + + // Expected order after sorting + expected := []string{ + "wlp2s0", // Wireless LAN on PCI bus + "wlx001122334455", // Wireless LAN with MAC address + "wlan0", // Old-style Wireless LAN + "eno1", // Highest priority (onboard Ethernet) + "enp0s3", // PCI bus Ethernet + "enx001122334455", // Ethernet with MAC address + "ens2", // Hotplug Ethernet + "eth0", // Old-style Ethernet + "tun0", // VPN TUN interface + "tap0", // VPN TAP interface + "awdl0", + } + + // Sorting the interfaces using the Compare method + sort.Slice(interfaces, func(i, j int) bool { + return sorter.Compare(interfaces[i], interfaces[j]) < 0 + }) + + // Assert the sorted order matches the expected order + assert.Equal(t, expected, interfaces, "The interfaces should be sorted correctly according to priority.") +} diff --git a/net/addrs/interface.go b/net/addrs/interface.go index 435100136..d38c5c552 100644 --- a/net/addrs/interface.go +++ b/net/addrs/interface.go @@ -6,8 +6,6 @@ package addrs import ( "net" "slices" - - "github.com/anyproto/anytype-heart/util/slice" ) func SetInterfaceAddrsGetter(getter InterfaceAddrsGetter) {} @@ -32,11 +30,7 @@ func GetInterfacesAddrs() (iAddrs InterfacesAddrs, err error) { if err != nil { return } - iAddrs.Interfaces = WrapInterfaces(ifaces) - - iAddrs.Interfaces = slice.Filter(iAddrs.Interfaces, func(iface NetInterfaceWithAddrCache) bool { - return iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagMulticast != 0 - }) + iAddrs.Interfaces = filterInterfaces(WrapInterfaces(ifaces)) return } diff --git a/net/addrs/interface_android.go b/net/addrs/interface_android.go index 10570ff56..11d6b9283 100644 --- a/net/addrs/interface_android.go +++ b/net/addrs/interface_android.go @@ -58,7 +58,6 @@ func GetInterfacesAddrs() (addrs InterfacesAddrs, err error) { lock.Unlock() for _, iface := range interfaceGetter.Interfaces() { ifaceWrapped := WrapInterface(iface.Interface) - addrs.Interfaces = append(addrs.Interfaces, WrapInterface(iface.Interface)) unmaskedAddrs := iface.Addrs ifaceAddrs := make([]net.Addr, 0, len(unmaskedAddrs)) for _, addr := range unmaskedAddrs { @@ -76,6 +75,9 @@ func GetInterfacesAddrs() (addrs InterfacesAddrs, err error) { // inject cached addresses, because we can't get them from net.Interface's Addrs() on android ifaceWrapped.cachedAddrs = ifaceAddrs addrs.Addrs = append(addrs.Addrs, ifaceAddrs...) + addrs.Interfaces = append(addrs.Interfaces, ifaceWrapped) } + + addrs.Interfaces = filterInterfaces(addrs.Interfaces) return } diff --git a/space/spacecore/localdiscovery/localdiscovery.go b/space/spacecore/localdiscovery/localdiscovery.go index e1518b15a..0855aa892 100644 --- a/space/spacecore/localdiscovery/localdiscovery.go +++ b/space/spacecore/localdiscovery/localdiscovery.go @@ -23,10 +23,10 @@ import ( "github.com/anyproto/anytype-heart/space/spacecore/clientserver" ) -var interfacesSortPriority = []string{"en", "wlan", "wl", "eth", "lo"} - type Hook int +var interfacesSortPriority = []string{"wlan", "wl", "en", "eth", "tun", "tap", "utun", "lo"} + const ( PeerToPeerImpossible Hook = 0 PeerToPeerPossible Hook = 1 @@ -70,7 +70,7 @@ func (l *localDiscovery) Init(a *app.App) (err error) { l.manualStart = a.MustComponent(config.CName).(*config.Config).DontStartLocalNetworkSyncAutomatically l.nodeConf = a.MustComponent(config.CName).(*config.Config).GetNodeConf() l.peerId = a.MustComponent(accountservice.CName).(accountservice.Service).Account().PeerId - l.periodicCheck = periodicsync.NewPeriodicSync(10, 0, l.checkAddrs, log) + l.periodicCheck = periodicsync.NewPeriodicSync(5, 0, l.checkAddrs, log) l.drpcServer = app.MustComponent[clientserver.ClientServer](a) return } @@ -158,10 +158,11 @@ func (l *localDiscovery) checkAddrs(ctx context.Context) (err error) { newAddrs, err := addrs.GetInterfacesAddrs() l.notifyPeerToPeerStatus(newAddrs) if err != nil { - return + return fmt.Errorf("getting iface addresses: %w", err) } - newAddrs.SortWithPriority(interfacesSortPriority) + newAddrs.SortInterfacesWithPriority(interfacesSortPriority) + if newAddrs.Equal(l.interfacesAddrs) && l.server != nil { return } @@ -174,22 +175,45 @@ func (l *localDiscovery) checkAddrs(ctx context.Context) (err error) { } l.ctx, l.cancel = context.WithCancel(ctx) if err = l.startServer(); err != nil { - return + return fmt.Errorf("starting mdns server: %w", err) } l.startQuerying(l.ctx) return } +func (l *localDiscovery) getAddresses() (ipv4, ipv6 []gonet.IP) { + for _, iface := range l.interfacesAddrs.Interfaces { + for _, addr := range iface.GetAddr() { + ip := addr.(*gonet.IPNet).IP + if ip.To4() != nil { + ipv4 = append(ipv4, ip) + } else { + ipv6 = append(ipv6, ip) + } + } + } + + if len(ipv4) == 0 { + // fallback in case we have no ipv4 addresses from interfaces + for _, addr := range l.interfacesAddrs.Addrs { + ip := strings.Split(addr.String(), "/")[0] + ipVal := gonet.ParseIP(ip) + if ipVal.To4() != nil { + ipv4 = append(ipv4, ipVal) + } else { + ipv6 = append(ipv6, ipVal) + } + } + l.interfacesAddrs.SortIPsLikeInterfaces(ipv4) + } + return +} + func (l *localDiscovery) startServer() (err error) { l.ipv4 = l.ipv4[:0] - l.ipv6 = l.ipv6[:0] - for _, addr := range l.interfacesAddrs.Addrs { - ip := strings.Split(addr.String(), "/")[0] - if gonet.ParseIP(ip).To4() != nil { - l.ipv4 = append(l.ipv4, ip) - } else { - l.ipv6 = append(l.ipv6, ip) - } + ipv4, _ := l.getAddresses() // ignore ipv6 for now + for _, ip := range ipv4 { + l.ipv4 = append(l.ipv4, ip.String()) } log.Debug("starting mdns server", zap.Strings("ips", l.ipv4), zap.Int("port", l.port), zap.String("peerId", l.peerId)) l.server, err = zeroconf.RegisterProxy( @@ -249,7 +273,7 @@ func (l *localDiscovery) browse(ctx context.Context, ch chan *zeroconf.ServiceEn if err != nil { return } - newAddrs.SortWithPriority(interfacesSortPriority) + newAddrs.SortInterfacesWithPriority(interfacesSortPriority) if err := zeroconf.Browse(ctx, serviceName, mdnsDomain, ch, zeroconf.ClientWriteTimeout(time.Second*1), zeroconf.SelectIfaces(newAddrs.NetInterfaces()), diff --git a/space/spacecore/localdiscovery/localdiscovery_android.go b/space/spacecore/localdiscovery/localdiscovery_android.go index 665d389a3..72dc22821 100644 --- a/space/spacecore/localdiscovery/localdiscovery_android.go +++ b/space/spacecore/localdiscovery/localdiscovery_android.go @@ -160,7 +160,7 @@ func (l *localDiscovery) notifyPeerToPeerStatus(newAddrs addrs.InterfacesAddrs) } func (l *localDiscovery) notifyP2PNotPossible(newAddrs addrs.InterfacesAddrs) bool { - return len(newAddrs.Interfaces) == 0 || IsLoopBack(newAddrs.Interfaces) + return len(newAddrs.Interfaces) == 0 || IsLoopBack(newAddrs.NetInterfaces()) } func IsLoopBack(interfaces []net.Interface) bool {