117 lines
3.7 KiB
Go
117 lines
3.7 KiB
Go
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
|
|
}
|