|
| 1 | +//go:build android |
| 2 | + |
| 3 | +package discovery |
| 4 | + |
| 5 | +// Android-specific mDNS implementation that avoids net.Interfaces() (blocked by |
| 6 | +// SELinux on Android) by using anet.Interfaces() instead. |
| 7 | +// |
| 8 | +// go-libp2p's mdns.NewMdnsService always passes nil to zeroconf.RegisterProxy and |
| 9 | +// zeroconf.Browse, which causes both to call listMulticastInterfaces() → net.Interfaces() |
| 10 | +// → syscall.NetlinkRIB → bind() on netlink_route_socket → EACCES on Android. |
| 11 | +// |
| 12 | +// We bypass this by calling zeroconf.RegisterProxy and zeroconf.Browse ourselves |
| 13 | +// with explicit interfaces obtained from anet, which uses sendto() instead of bind() |
| 14 | +// on the netlink socket. |
| 15 | +// |
| 16 | +// We also avoid h.Addrs() at startup because the address manager's background goroutine |
| 17 | +// may not have populated currentAddrs.localAddrs yet. Instead we derive IPs directly from |
| 18 | +// anet.InterfaceAddrs() and ports from h.Network().ListenAddresses(). |
| 19 | + |
| 20 | +import ( |
| 21 | + "context" |
| 22 | + "math/rand" |
| 23 | + "net" |
| 24 | + "strings" |
| 25 | + |
| 26 | + "github.com/ipfs/go-log" |
| 27 | + "github.com/libp2p/go-libp2p/core/host" |
| 28 | + "github.com/libp2p/go-libp2p/core/peer" |
| 29 | + "github.com/libp2p/zeroconf/v2" |
| 30 | + ma "github.com/multiformats/go-multiaddr" |
| 31 | + manet "github.com/multiformats/go-multiaddr/net" |
| 32 | + "github.com/wlynxg/anet" |
| 33 | +) |
| 34 | + |
| 35 | +const ( |
| 36 | + mdnsServiceName = "_p2p._udp" |
| 37 | + mdnsDomain = "local" |
| 38 | + dnsaddrPrefix = "dnsaddr=" |
| 39 | +) |
| 40 | + |
| 41 | +func (d *MDNS) Run(l log.StandardLogger, ctx context.Context, h host.Host) error { |
| 42 | + serviceName := d.DiscoveryServiceTag |
| 43 | + if serviceName == "" { |
| 44 | + serviceName = mdnsServiceName |
| 45 | + } |
| 46 | + l.Infof("mdns(android): starting, service=%s", serviceName) |
| 47 | + |
| 48 | + // Get network interfaces without calling net.Interfaces() (blocked on Android). |
| 49 | + ifaces, err := anet.Interfaces() |
| 50 | + if err != nil { |
| 51 | + return err |
| 52 | + } |
| 53 | + |
| 54 | + // Keep only interfaces that are up and support multicast. |
| 55 | + var mcIfaces []net.Interface |
| 56 | + for _, iface := range ifaces { |
| 57 | + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagMulticast == 0 { |
| 58 | + continue |
| 59 | + } |
| 60 | + mcIfaces = append(mcIfaces, iface) |
| 61 | + } |
| 62 | + if len(mcIfaces) == 0 { |
| 63 | + l.Warnf("mdns(android): no multicast-capable interfaces found (WiFi off?), mDNS skipped") |
| 64 | + return nil |
| 65 | + } |
| 66 | + l.Infof("mdns(android): using interfaces: %v", ifaceNames(mcIfaces)) |
| 67 | + |
| 68 | + // Get real interface IPs via anet (avoids netlinkrib bind() call). |
| 69 | + // These are used both for the mDNS A/AAAA records and for expanding |
| 70 | + // wildcard/loopback listen addresses into routable addresses. |
| 71 | + ifAddrs, err := anet.InterfaceAddrs() |
| 72 | + if err != nil { |
| 73 | + return err |
| 74 | + } |
| 75 | + var routableIPs []net.IP |
| 76 | + for _, a := range ifAddrs { |
| 77 | + ipNet, ok := a.(*net.IPNet) |
| 78 | + if !ok { |
| 79 | + continue |
| 80 | + } |
| 81 | + ip := ipNet.IP |
| 82 | + if ip.IsLoopback() || ip.IsLinkLocalUnicast() { |
| 83 | + continue |
| 84 | + } |
| 85 | + routableIPs = append(routableIPs, ip) |
| 86 | + } |
| 87 | + if len(routableIPs) == 0 { |
| 88 | + l.Warnf("mdns(android): no routable interface addresses found, mDNS skipped") |
| 89 | + return nil |
| 90 | + } |
| 91 | + |
| 92 | + // Build multiaddrs for each routable IP (without port — used as base for expansion). |
| 93 | + var routableMaddrs []ma.Multiaddr |
| 94 | + for _, ip := range routableIPs { |
| 95 | + maddr, err := manet.FromIP(ip) |
| 96 | + if err != nil { |
| 97 | + continue |
| 98 | + } |
| 99 | + routableMaddrs = append(routableMaddrs, maddr) |
| 100 | + } |
| 101 | + |
| 102 | + // Get the raw listen addresses (e.g. /ip4/0.0.0.0/tcp/PORT or /ip4/127.0.0.1/tcp/PORT). |
| 103 | + // These may be wildcards or loopback; we expand them to real IPs below. |
| 104 | + listenAddrs := h.Network().ListenAddresses() |
| 105 | + |
| 106 | + // Expand wildcard/loopback listen addresses to routable IPs, keeping transport/port. |
| 107 | + var expandedListenAddrs []ma.Multiaddr |
| 108 | + for _, la := range listenAddrs { |
| 109 | + ip, err := manet.ToIP(la) |
| 110 | + if err != nil { |
| 111 | + // Non-IP address (e.g. /p2p-circuit); skip for mDNS. |
| 112 | + continue |
| 113 | + } |
| 114 | + if !ip.IsUnspecified() && !ip.IsLoopback() { |
| 115 | + // Already a routable address. |
| 116 | + expandedListenAddrs = append(expandedListenAddrs, la) |
| 117 | + continue |
| 118 | + } |
| 119 | + // Wildcard or loopback → expand to real IPs with same transport/port. |
| 120 | + _, transport := ma.SplitFirst(la) |
| 121 | + for _, rAddr := range routableMaddrs { |
| 122 | + expanded := rAddr |
| 123 | + if transport != nil { |
| 124 | + expanded = rAddr.Encapsulate(transport) |
| 125 | + } |
| 126 | + expandedListenAddrs = append(expandedListenAddrs, expanded) |
| 127 | + } |
| 128 | + } |
| 129 | + if len(expandedListenAddrs) == 0 { |
| 130 | + l.Warnf("mdns(android): no listen addresses to advertise, mDNS skipped") |
| 131 | + return nil |
| 132 | + } |
| 133 | + |
| 134 | + // Build p2p multiaddrs (with /p2p/PeerID suffix) for TXT records. |
| 135 | + p2pAddrs, err := peer.AddrInfoToP2pAddrs(&peer.AddrInfo{ |
| 136 | + ID: h.ID(), |
| 137 | + Addrs: expandedListenAddrs, |
| 138 | + }) |
| 139 | + if err != nil { |
| 140 | + return err |
| 141 | + } |
| 142 | + |
| 143 | + var txts []string |
| 144 | + for _, addr := range p2pAddrs { |
| 145 | + if isSuitableForMDNS(addr) { |
| 146 | + txts = append(txts, dnsaddrPrefix+addr.String()) |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + // Build the IP strings for the mDNS A/AAAA records. |
| 151 | + ips := ipsToStrings(routableIPs) |
| 152 | + |
| 153 | + peerName := randomString(32 + rand.Intn(32)) |
| 154 | + |
| 155 | + l.Infof("mdns(android): advertising IPs=%v txts=%v", ips, txts) |
| 156 | + server, err := zeroconf.RegisterProxy( |
| 157 | + peerName, |
| 158 | + serviceName, |
| 159 | + mdnsDomain, |
| 160 | + 4001, // port is carried in TXT records; this value is required but ignored by libp2p peers |
| 161 | + peerName, |
| 162 | + ips, |
| 163 | + txts, |
| 164 | + mcIfaces, |
| 165 | + ) |
| 166 | + if err != nil { |
| 167 | + return err |
| 168 | + } |
| 169 | + l.Infof("mdns(android): registered proxy, browsing for peers...") |
| 170 | + |
| 171 | + // Browse for peers on the same service. SelectIfaces bypasses listMulticastInterfaces(). |
| 172 | + entryChan := make(chan *zeroconf.ServiceEntry, 1000) |
| 173 | + |
| 174 | + errCh := make(chan error, 1) |
| 175 | + go func() { |
| 176 | + errCh <- zeroconf.Browse(ctx, serviceName, mdnsDomain, entryChan, zeroconf.SelectIfaces(mcIfaces)) |
| 177 | + }() |
| 178 | + |
| 179 | + notifee := &discoveryNotifee{h: h, c: l} |
| 180 | + |
| 181 | + // Run the peer-handling loop in the background so Run() returns immediately, |
| 182 | + // matching the non-android implementation (mdns_run.go) which is also non-blocking. |
| 183 | + // startNetwork() must return for the ledger and VPN network services to start. |
| 184 | + go func() { |
| 185 | + defer server.Shutdown() |
| 186 | + for { |
| 187 | + select { |
| 188 | + case entry, ok := <-entryChan: |
| 189 | + if !ok { |
| 190 | + return |
| 191 | + } |
| 192 | + var addrs []ma.Multiaddr |
| 193 | + for _, txt := range entry.Text { |
| 194 | + if !strings.HasPrefix(txt, dnsaddrPrefix) { |
| 195 | + continue |
| 196 | + } |
| 197 | + addr, err := ma.NewMultiaddr(txt[len(dnsaddrPrefix):]) |
| 198 | + if err != nil { |
| 199 | + continue |
| 200 | + } |
| 201 | + addrs = append(addrs, addr) |
| 202 | + } |
| 203 | + infos, err := peer.AddrInfosFromP2pAddrs(addrs...) |
| 204 | + if err != nil { |
| 205 | + continue |
| 206 | + } |
| 207 | + for _, info := range infos { |
| 208 | + if info.ID == h.ID() { |
| 209 | + continue |
| 210 | + } |
| 211 | + l.Infof("mdns(android): found peer %s addrs=%v", info.ID, info.Addrs) |
| 212 | + go notifee.HandlePeerFound(info) |
| 213 | + } |
| 214 | + case err := <-errCh: |
| 215 | + if err != nil { |
| 216 | + l.Warnf("mdns(android): browse error: %v", err) |
| 217 | + } |
| 218 | + return |
| 219 | + case <-ctx.Done(): |
| 220 | + return |
| 221 | + } |
| 222 | + } |
| 223 | + }() |
| 224 | + |
| 225 | + return nil |
| 226 | +} |
| 227 | + |
| 228 | +// ifaceNames returns the names of the given interfaces for logging. |
| 229 | +func ifaceNames(ifaces []net.Interface) []string { |
| 230 | + names := make([]string, 0, len(ifaces)) |
| 231 | + for _, iface := range ifaces { |
| 232 | + names = append(names, iface.Name) |
| 233 | + } |
| 234 | + return names |
| 235 | +} |
| 236 | + |
| 237 | +// ipsToStrings converts net.IP slice to string slice (IPv4 as dotted decimal, IPv6 as standard). |
| 238 | +func ipsToStrings(ips []net.IP) []string { |
| 239 | + ss := make([]string, 0, len(ips)) |
| 240 | + for _, ip := range ips { |
| 241 | + ss = append(ss, ip.String()) |
| 242 | + } |
| 243 | + return ss |
| 244 | +} |
| 245 | + |
| 246 | +// isSuitableForMDNS mirrors the same function from go-libp2p's mdns package. |
| 247 | +// It filters multiaddrs to those suitable for LAN mDNS advertisement. |
| 248 | +func isSuitableForMDNS(addr ma.Multiaddr) bool { |
| 249 | + if addr == nil { |
| 250 | + return false |
| 251 | + } |
| 252 | + first, _ := ma.SplitFirst(addr) |
| 253 | + if first == nil { |
| 254 | + return false |
| 255 | + } |
| 256 | + switch first.Protocol().Code { |
| 257 | + case ma.P_IP4, ma.P_IP6: |
| 258 | + // ok |
| 259 | + case ma.P_DNS, ma.P_DNS4, ma.P_DNS6, ma.P_DNSADDR: |
| 260 | + if !strings.HasSuffix(strings.ToLower(first.Value()), ".local") { |
| 261 | + return false |
| 262 | + } |
| 263 | + default: |
| 264 | + return false |
| 265 | + } |
| 266 | + // Reject circuit relay and browser-only transports. |
| 267 | + unsuitable := false |
| 268 | + ma.ForEach(addr, func(c ma.Component) bool { |
| 269 | + switch c.Protocol().Code { |
| 270 | + case ma.P_CIRCUIT, ma.P_WEBTRANSPORT, ma.P_WEBRTC, ma.P_WEBRTC_DIRECT, ma.P_P2P_WEBRTC_DIRECT, ma.P_WS, ma.P_WSS: |
| 271 | + unsuitable = true |
| 272 | + return false |
| 273 | + } |
| 274 | + return true |
| 275 | + }) |
| 276 | + return !unsuitable |
| 277 | +} |
| 278 | + |
| 279 | +// randomString generates a random lowercase alphanumeric string of length l. |
| 280 | +func randomString(l int) string { |
| 281 | + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789" |
| 282 | + s := make([]byte, 0, l) |
| 283 | + for i := 0; i < l; i++ { |
| 284 | + s = append(s, alphabet[rand.Intn(len(alphabet))]) |
| 285 | + } |
| 286 | + return string(s) |
| 287 | +} |
0 commit comments