oddjob@zinken:~/flacs % cat hitta4.go package main import ( "bufio" "flag" "fmt" "net" "net/url" "os" "strings" "time" ) var ( ifaceName = flag.String("iface", "", "LAN interface to bind (e.g. igb0, em0)") srcIP = flag.String("src", "", "bind to this IPv4 (overrides -iface)") host = flag.String("host", "", "probe this single host (unicast)") timeout = flag.Duration("timeout", 4*time.Second, "receive window") retries = flag.Int("retries", 2, "multicast retry count") mx = flag.Int("mx", 2, "MX seconds for M-SEARCH") debug = flag.Bool("debug", false, "verbose logs") ) var sts = []string{ "urn:av-openhome-org:service:Product:2", "urn:av-openhome-org:service:Product:1", "urn:av-openhome-org:service:Playlist:1", "urn:schemas-upnp-org:device:MediaRenderer:1", "upnp:rootdevice", } type dev struct{ UUID, IP, Location string; Port int } func main() { flag.Parse() ip, ipNet, err := pickBindIP(*ifaceName, *srcIP) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if *debug { fmt.Printf("Bind IP: %s, Subnet: %v\n", ip, ipNet) } found := map[string]dev{} // 1) Multicast search multicastSearch(ip, found) // 2) Fallback unicast if len(found) == 0 { if *host != "" { unicastProbe(ip, net.ParseIP(*host), found) } else { iterSubnet(ipNet, func(dst net.IP) { unicastProbe(ip, dst, found) }) } time.Sleep(300 * time.Millisecond) } if len(found) == 0 { fmt.Println("No OpenHome devices found.") fmt.Println("Hints:") fmt.Println(" • Use -iface or -src ") fmt.Println(" • PF may block UDP replies; test briefly with: sudo pfctl -d (then pfctl -e)") fmt.Println(" • Some APs neuter multicast; unicast (-host / subnet sweep) should still work") return } for _, d := range found { fmt.Printf("UUID=%s IP=%s Port=%d\n", d.UUID, d.IP, d.Port) fmt.Printf(" LOCATION: %s\n", d.Location) fmt.Printf(" Playlist: %s\n", ohCtlPath(d.UUID, "urn:av-openhome-org:service:Playlist:1")) fmt.Printf(" Product : %s\n", ohCtlPath(d.UUID, "urn:av-openhome-org:service:Product:2")) } } func pickBindIP(ifName, ipStr string) (net.IP, *net.IPNet, error) { if ipStr != "" { ip := net.ParseIP(ipStr) if ip == nil || ip.To4() == nil { return nil, nil, fmt.Errorf("invalid -src IPv4: %q", ipStr) } // guess /24 if mask is unknown _, ipNet, _ := net.ParseCIDR(ip.String() + "/24") return ip.To4(), ipNet, nil } ifs, _ := net.Interfaces() for _, ifc := range ifs { if (ifc.Flags&net.FlagUp) == 0 || (ifc.Flags&net.FlagLoopback) != 0 { continue } if ifName != "" && ifc.Name != ifName { continue } addrs, _ := ifc.Addrs() for _, a := range addrs { if ipnet, ok := a.(*net.IPNet); ok && ipnet.IP != nil && ipnet.IP.To4() != nil { return ipnet.IP.To4(), ipnet, nil } } } return nil, nil, fmt.Errorf("no usable IPv4 on interface %q", ifName) } func multicastSearch(bindIP net.IP, found map[string]dev) { laddr := &net.UDPAddr{IP: bindIP, Port: 0} conn, err := net.ListenUDP("udp4", laddr) if err != nil { return } defer conn.Close() dst := &net.UDPAddr{IP: net.IPv4(239, 255, 255, 250), Port: 1900} reqs := buildRequests() for i := 0; i < *retries; i++ { for _, r := range reqs { _, _ = conn.WriteToUDP([]byte(r), dst) } } _ = conn.SetReadDeadline(time.Now().Add(*timeout)) buf := make([]byte, 8192) for { n, _, err := conn.ReadFromUDP(buf) if err != nil { break } parseResponse(string(buf[:n]), found) } } func unicastProbe(bindIP, dstIP net.IP, found map[string]dev) { if dstIP == nil || dstIP.IsLoopback() || dstIP.IsMulticast() { return } laddr := &net.UDPAddr{IP: bindIP, Port: 0} raddr := &net.UDPAddr{IP: dstIP, Port: 1900} conn, err := net.ListenUDP("udp4", laddr) if err != nil { return } defer conn.Close() reqs := buildRequests() for _, r := range reqs { _, _ = conn.WriteToUDP([]byte(r), raddr) } _ = conn.SetReadDeadline(time.Now().Add(300 * time.Millisecond)) buf := make([]byte, 8192) for { n, _, err := conn.ReadFromUDP(buf) if err != nil { break } parseResponse(string(buf[:n]), found) } } func buildRequests() []string { var out []string for _, st := range sts { r := strings.Join([]string{ "M-SEARCH * HTTP/1.1", "HOST: 239.255.255.250:1900", "MAN: \"ssdp:discover\"", fmt.Sprintf("MX: %d", *mx), "ST: " + st, "", "", }, "\r\n") out = append(out, r) } return out } func parseResponse(s string, found map[string]dev) { h := parseHeaders(s) usn := strings.TrimSpace(h["usn"]) loc := strings.TrimSpace(h["location"]) if usn == "" || loc == "" { return } uuid := extractUUID(usn) if uuid == "" { return } u, err := url.Parse(loc) if err != nil { return } host := u.Hostname() port := 80 if p := u.Port(); p != "" { fmt.Sscanf(p, "%d", &port) } found[uuid] = dev{UUID: uuid, IP: host, Port: port, Location: loc} } func parseHeaders(s string) map[string]string { m := map[string]string{} sc := bufio.NewScanner(strings.NewReader(s)) for sc.Scan() { line := sc.Text() if i := strings.IndexByte(line, ':'); i > 0 { k := strings.ToLower(strings.TrimSpace(line[:i])) v := strings.TrimSpace(line[i+1:]) m[k] = v } } return m } func extractUUID(usn string) string { i := strings.Index(usn, "uuid:") if i < 0 { return "" } u := usn[i+5:] if j := strings.Index(u, "::"); j >= 0 { u = u[:j] } return strings.TrimSpace(u) } func ohCtlPath(uuid, service string) string { return "/uuid-" + uuid + "/ctl-" + strings.ReplaceAll(service, ":", "-") } func iterSubnet(n *net.IPNet, fn func(net.IP)) { if n == nil { return } ip := n.IP.Mask(n.Mask).To4() if ip == nil { return } bcast := net.IP{ip[0] | ^n.Mask[0], ip[1] | ^n.Mask[1], ip[2] | ^n.Mask[2], ip[3] | ^n.Mask[3]} for cur := inc(ip); less(cur, bcast); cur = inc(cur) { fn(cur) } } func inc(ip net.IP) net.IP { x := append(net.IP(nil), ip...); for i := 3; i >= 0; i-- { x[i]++; if x[i] != 0 { break } } ; return x } func less(a, b net.IP) bool { for i := 0; i < 4; i++ { if a[i] < b[i] { return true }; if a[i] > b[i] { return false } } ; return false }