Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func peers2AddrInfo(peers []string) []peer.AddrInfo {
var infiniteResourceLimits = rcmgr.InfiniteLimits.ToPartialLimitConfig().System

// ToOpts returns node and vpn options from a configuration
func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) {
func (c Config) ToOpts(l log.StandardLogger) ([]node.Option, []vpn.Option, error) {

if err := c.Validate(); err != nil {
return nil, nil, err
Expand All @@ -190,7 +190,11 @@ func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) {
lvl = log.LevelError
}

llger := logger.New(lvl)
// Use the caller-provided logger if given, otherwise create a default one.
llger := l
if llger == nil {
llger = logger.New(lvl)
}

libp2plvl, err := log.LevelFromString(libp2plogLevel)
if err != nil {
Expand Down
8 changes: 0 additions & 8 deletions pkg/discovery/mdns.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (

"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
)

type MDNS struct {
Expand All @@ -48,10 +47,3 @@ func (n *discoveryNotifee) HandlePeerFound(pi peer.AddrInfo) {
func (d *MDNS) Option(ctx context.Context) func(c *libp2p.Config) error {
return func(*libp2p.Config) error { return nil }
}

func (d *MDNS) Run(l log.StandardLogger, ctx context.Context, host host.Host) error {
// setup mDNS discovery to find local peers

disc := mdns.NewMdnsService(host, d.DiscoveryServiceTag, &discoveryNotifee{h: host, c: l})
return disc.Start()
}
287 changes: 287 additions & 0 deletions pkg/discovery/mdns_android.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
//go:build android

package discovery

// Android-specific mDNS implementation that avoids net.Interfaces() (blocked by
// SELinux on Android) by using anet.Interfaces() instead.
//
// go-libp2p's mdns.NewMdnsService always passes nil to zeroconf.RegisterProxy and
// zeroconf.Browse, which causes both to call listMulticastInterfaces() → net.Interfaces()
// → syscall.NetlinkRIB → bind() on netlink_route_socket → EACCES on Android.
//
// We bypass this by calling zeroconf.RegisterProxy and zeroconf.Browse ourselves
// with explicit interfaces obtained from anet, which uses sendto() instead of bind()
// on the netlink socket.
//
// We also avoid h.Addrs() at startup because the address manager's background goroutine
// may not have populated currentAddrs.localAddrs yet. Instead we derive IPs directly from
// anet.InterfaceAddrs() and ports from h.Network().ListenAddresses().

import (
"context"
"math/rand"
"net"
"strings"

"github.com/ipfs/go-log"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/zeroconf/v2"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/wlynxg/anet"
)

const (
mdnsServiceName = "_p2p._udp"
mdnsDomain = "local"
dnsaddrPrefix = "dnsaddr="
)

func (d *MDNS) Run(l log.StandardLogger, ctx context.Context, h host.Host) error {
serviceName := d.DiscoveryServiceTag
if serviceName == "" {
serviceName = mdnsServiceName
}
l.Infof("mdns(android): starting, service=%s", serviceName)

// Get network interfaces without calling net.Interfaces() (blocked on Android).
ifaces, err := anet.Interfaces()
if err != nil {
return err
}

// Keep only interfaces that are up and support multicast.
var mcIfaces []net.Interface
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagMulticast == 0 {
continue
}
mcIfaces = append(mcIfaces, iface)
}
if len(mcIfaces) == 0 {
l.Warnf("mdns(android): no multicast-capable interfaces found (WiFi off?), mDNS skipped")
return nil
}
l.Infof("mdns(android): using interfaces: %v", ifaceNames(mcIfaces))

// Get real interface IPs via anet (avoids netlinkrib bind() call).
// These are used both for the mDNS A/AAAA records and for expanding
// wildcard/loopback listen addresses into routable addresses.
ifAddrs, err := anet.InterfaceAddrs()
if err != nil {
return err
}
var routableIPs []net.IP
for _, a := range ifAddrs {
ipNet, ok := a.(*net.IPNet)
if !ok {
continue
}
ip := ipNet.IP
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
continue
}
routableIPs = append(routableIPs, ip)
}
if len(routableIPs) == 0 {
l.Warnf("mdns(android): no routable interface addresses found, mDNS skipped")
return nil
}

// Build multiaddrs for each routable IP (without port — used as base for expansion).
var routableMaddrs []ma.Multiaddr
for _, ip := range routableIPs {
maddr, err := manet.FromIP(ip)
if err != nil {
continue
}
routableMaddrs = append(routableMaddrs, maddr)
}

// Get the raw listen addresses (e.g. /ip4/0.0.0.0/tcp/PORT or /ip4/127.0.0.1/tcp/PORT).
// These may be wildcards or loopback; we expand them to real IPs below.
listenAddrs := h.Network().ListenAddresses()

// Expand wildcard/loopback listen addresses to routable IPs, keeping transport/port.
var expandedListenAddrs []ma.Multiaddr
for _, la := range listenAddrs {
ip, err := manet.ToIP(la)
if err != nil {
// Non-IP address (e.g. /p2p-circuit); skip for mDNS.
continue
}
if !ip.IsUnspecified() && !ip.IsLoopback() {
// Already a routable address.
expandedListenAddrs = append(expandedListenAddrs, la)
continue
}
// Wildcard or loopback → expand to real IPs with same transport/port.
_, transport := ma.SplitFirst(la)
for _, rAddr := range routableMaddrs {
expanded := rAddr
if transport != nil {
expanded = rAddr.Encapsulate(transport)
}
expandedListenAddrs = append(expandedListenAddrs, expanded)
}
}
if len(expandedListenAddrs) == 0 {
l.Warnf("mdns(android): no listen addresses to advertise, mDNS skipped")
return nil
}

// Build p2p multiaddrs (with /p2p/PeerID suffix) for TXT records.
p2pAddrs, err := peer.AddrInfoToP2pAddrs(&peer.AddrInfo{
ID: h.ID(),
Addrs: expandedListenAddrs,
})
if err != nil {
return err
}

var txts []string
for _, addr := range p2pAddrs {
if isSuitableForMDNS(addr) {
txts = append(txts, dnsaddrPrefix+addr.String())
}
}

// Build the IP strings for the mDNS A/AAAA records.
ips := ipsToStrings(routableIPs)

peerName := randomString(32 + rand.Intn(32))

l.Infof("mdns(android): advertising IPs=%v txts=%v", ips, txts)
server, err := zeroconf.RegisterProxy(
peerName,
serviceName,
mdnsDomain,
4001, // port is carried in TXT records; this value is required but ignored by libp2p peers
peerName,
ips,
txts,
mcIfaces,
)
if err != nil {
return err
}
l.Infof("mdns(android): registered proxy, browsing for peers...")

// Browse for peers on the same service. SelectIfaces bypasses listMulticastInterfaces().
entryChan := make(chan *zeroconf.ServiceEntry, 1000)

errCh := make(chan error, 1)
go func() {
errCh <- zeroconf.Browse(ctx, serviceName, mdnsDomain, entryChan, zeroconf.SelectIfaces(mcIfaces))
}()

notifee := &discoveryNotifee{h: h, c: l}

// Run the peer-handling loop in the background so Run() returns immediately,
// matching the non-android implementation (mdns_run.go) which is also non-blocking.
// startNetwork() must return for the ledger and VPN network services to start.
go func() {
defer server.Shutdown()
for {
select {
case entry, ok := <-entryChan:
if !ok {
return
}
var addrs []ma.Multiaddr
for _, txt := range entry.Text {
if !strings.HasPrefix(txt, dnsaddrPrefix) {
continue
}
addr, err := ma.NewMultiaddr(txt[len(dnsaddrPrefix):])
if err != nil {
continue
}
addrs = append(addrs, addr)
}
infos, err := peer.AddrInfosFromP2pAddrs(addrs...)
if err != nil {
continue
}
for _, info := range infos {
if info.ID == h.ID() {
continue
}
l.Infof("mdns(android): found peer %s addrs=%v", info.ID, info.Addrs)
go notifee.HandlePeerFound(info)
}
case err := <-errCh:
if err != nil {
l.Warnf("mdns(android): browse error: %v", err)
}
return
case <-ctx.Done():
return
}
}
}()

return nil
}

// ifaceNames returns the names of the given interfaces for logging.
func ifaceNames(ifaces []net.Interface) []string {
names := make([]string, 0, len(ifaces))
for _, iface := range ifaces {
names = append(names, iface.Name)
}
return names
}

// ipsToStrings converts net.IP slice to string slice (IPv4 as dotted decimal, IPv6 as standard).
func ipsToStrings(ips []net.IP) []string {
ss := make([]string, 0, len(ips))
for _, ip := range ips {
ss = append(ss, ip.String())
}
return ss
}

// isSuitableForMDNS mirrors the same function from go-libp2p's mdns package.
// It filters multiaddrs to those suitable for LAN mDNS advertisement.
func isSuitableForMDNS(addr ma.Multiaddr) bool {
if addr == nil {
return false
}
first, _ := ma.SplitFirst(addr)
if first == nil {
return false
}
switch first.Protocol().Code {
case ma.P_IP4, ma.P_IP6:
// ok
case ma.P_DNS, ma.P_DNS4, ma.P_DNS6, ma.P_DNSADDR:
if !strings.HasSuffix(strings.ToLower(first.Value()), ".local") {
return false
}
default:
return false
}
// Reject circuit relay and browser-only transports.
unsuitable := false
ma.ForEach(addr, func(c ma.Component) bool {
switch c.Protocol().Code {
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:
unsuitable = true
return false
}
return true
})
return !unsuitable
}

// randomString generates a random lowercase alphanumeric string of length l.
func randomString(l int) string {
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
s := make([]byte, 0, l)
for i := 0; i < l; i++ {
s = append(s, alphabet[rand.Intn(len(alphabet))])
}
return string(s)
}
16 changes: 16 additions & 0 deletions pkg/discovery/mdns_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build !android

package discovery

import (
"context"

"github.com/ipfs/go-log"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
)

func (d *MDNS) Run(l log.StandardLogger, ctx context.Context, host host.Host) error {
disc := mdns.NewMdnsService(host, d.DiscoveryServiceTag, &discoveryNotifee{h: host, c: l})
return disc.Start()
}
5 changes: 4 additions & 1 deletion pkg/node/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,10 @@ func (e *Node) handleEvents(ctx context.Context, inputChannel chan *hub.Message,
c.Message = str

if err := pub(c); err != nil {
e.config.Logger.Warnf("publish error: %s", err)
// "no message room available" is normal during the first ~1s after start (hub joins room after TOTP tick).
if err.Error() != "no message room available" {
e.config.Logger.Warnf("publish error: %s", err)
}
}

case m := <-roomMessages:
Expand Down
10 changes: 8 additions & 2 deletions pkg/vpn/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ limitations under the License.
package vpn

import (
"io"
"time"

"github.com/ipfs/go-log"
"github.com/mudler/water"
)

type Config struct {
Interface *water.Interface
Interface io.ReadWriteCloser
InterfaceName string
InterfaceAddress string
RouterAddress string
Expand Down Expand Up @@ -72,7 +73,12 @@ var LowProfile Option = func(cfg *Config) error {
return nil
}

func WithInterface(i *water.Interface) func(cfg *Config) error {
// WithInterface sets a pre-created io.ReadWriteCloser as the tunnel interface,
// bypassing platform-specific interface creation entirely. When set, neither
// createInterface nor prepareInterface will be called. This is useful on
// platforms like Android where the TUN file descriptor is provided by the OS
// (e.g. via VpnService) and cannot be opened directly.
func WithInterface(i io.ReadWriteCloser) func(cfg *Config) error {
return func(cfg *Config) error {
cfg.Interface = i
return nil
Expand Down
Loading