package remotecontrol import ( "errors" "fmt" "net" ) // ErrNoLAN is returned by DetectRoutableIP when no routable, non-loopback IPv4 // address can be found. Binding loopback would produce a URL the phone cannot // reach, so the caller must surface this rather than fall back silently. var ErrNoLAN = errors.New("remotecontrol: no routable LAN address found; are you connected to Wi-Fi?") // ifaceInfo pairs a network interface's flags with its addresses. DetectRoutableIP // consumes these so it can skip down / loopback / point-to-point (VPN tunnel) // interfaces before considering their addresses. type ifaceInfo struct { flags net.Flags addrs []net.Addr } // ifaceLister returns the host's interfaces with their addresses. It is a package // variable so tests can inject a fake set without real hardware. var ifaceLister = defaultIfaceLister func defaultIfaceLister() ([]ifaceInfo, error) { ifaces, err := net.Interfaces() if err != nil { return nil, err } out := make([]ifaceInfo, 0, len(ifaces)) for _, ifi := range ifaces { addrs, err := ifi.Addrs() if err != nil { continue // an interface whose addresses can't be read is unusable } out = append(out, ifaceInfo{flags: ifi.Flags, addrs: addrs}) } return out, nil } // DetectRoutableIP returns a routable IPv4 address suitable for embedding in the // printed pairing URL — one a phone on the same Wi-Fi can actually reach. // // It iterates interfaces (not bare addresses) so it can skip the ones that would // yield an unreachable URL: interfaces that are down, loopback, or point-to-point // (VPN utun / tunnel links, whose address the phone cannot route to). Among the // rest it prefers a private LAN address (RFC1918: 10/8, 172.16/12, 192.168/16), // which is what home/office Wi-Fi hands out; a non-private routable address is // used only as a fallback when no private one exists. Callers that need a specific // interface should override via Config.Host. It returns ErrNoLAN when nothing // routable exists. func DetectRoutableIP() (string, error) { ifaces, err := ifaceLister() if err != nil { return "", fmt.Errorf("remotecontrol: list interfaces: %w", err) } var fallback string for _, ifi := range ifaces { if ifi.flags&net.FlagUp == 0 { continue // interface is down } if ifi.flags&net.FlagLoopback != 0 { continue } if ifi.flags&net.FlagPointToPoint != 0 { continue // VPN / tunnel link — its address isn't reachable from the LAN } for _, a := range ifi.addrs { var ip net.IP switch v := a.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP default: continue } v4 := ip.To4() if v4 == nil { continue // skip IPv6 for the LAN URL } if v4.IsLoopback() || v4.IsLinkLocalUnicast() || v4.IsLinkLocalMulticast() || v4.IsUnspecified() { continue } if v4.IsPrivate() { return v4.String(), nil } if fallback == "" { fallback = v4.String() } } } if fallback != "" { return fallback, nil } return "", ErrNoLAN } // ListenFreePort binds a TCP listener on host. It first tries the requested // port; if port is 0 or already in use it falls back to a kernel-assigned free // port (:0). It returns the listener and the actual port bound. func ListenFreePort(host string, port int) (net.Listener, int, error) { if port != 0 { ln, err := net.Listen("tcp", net.JoinHostPort(host, fmt.Sprint(port))) if err == nil { return ln, ln.Addr().(*net.TCPAddr).Port, nil } // Requested port unavailable — fall through to an auto-assigned one. } ln, err := net.Listen("tcp", net.JoinHostPort(host, "0")) if err != nil { return nil, 0, fmt.Errorf("remotecontrol: bind %s: %w", host, err) } return ln, ln.Addr().(*net.TCPAddr).Port, nil }