package mdns import ( "context" "log" "net" "strings" "time" "github.com/grandcat/zeroconf" "imuslab.com/zoraxy/mod/utils" ) type MDNSHost struct { MDNS *zeroconf.Server Host *NetworkHost IfaceOverride *net.Interface } type NetworkHost struct { HostName string Port int IPv4 []net.IP Domain string Model string UUID string Vendor string BuildVersion string MacAddr []string Online bool } // Create a new MDNS discoverer, set MacOverride to empty string for using the first NIC discovered func NewMDNS(config NetworkHost, MacOverride string) (*MDNSHost, error) { //Get host MAC Address macAddress, err := getMacAddr() if err != nil { return nil, err } macAddressBoardcast := "" if err == nil { macAddressBoardcast = strings.Join(macAddress, ",") } else { log.Println("[mDNS] Unable to get MAC Address: ", err.Error()) } //Register the mds services server, err := zeroconf.Register(config.HostName, "_http._tcp", "local.", config.Port, []string{"version_build=" + config.BuildVersion, "vendor=" + config.Vendor, "model=" + config.Model, "uuid=" + config.UUID, "domain=" + config.Domain, "mac_addr=" + macAddressBoardcast}, nil) if err != nil { log.Println("[mDNS] Error when registering zeroconf broadcast message", err.Error()) return &MDNSHost{}, err } //Discover the iface to override if exists var overrideIface *net.Interface = nil if MacOverride != "" { ifaceIp := "" ifaces, err := net.Interfaces() if err != nil { log.Println("[mDNS] Unable to override iface MAC: " + err.Error() + ". Resuming with default iface") } foundMatching := false for _, iface := range ifaces { thisIfaceMac := iface.HardwareAddr.String() thisIfaceMac = strings.ReplaceAll(thisIfaceMac, ":", "-") MacOverride = strings.ReplaceAll(MacOverride, ":", "-") if strings.EqualFold(thisIfaceMac, strings.TrimSpace(MacOverride)) { //This is the correct iface to use overrideIface = &iface addrs, err := iface.Addrs() if err == nil && len(addrs) > 0 { ifaceIp = addrs[0].String() } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip.To4() != nil { //This NIC have Ipv4 addr ifaceIp = ip.String() } } foundMatching = true break } } if !foundMatching { log.Println("[mDNS] Unable to find the target iface with MAC address: " + MacOverride + ". Resuming with default iface") } else { log.Println("[mDNS] Entering force MAC address mode, listening on: " + MacOverride + "(IP address: " + ifaceIp + ")") } } return &MDNSHost{ MDNS: server, Host: &config, IfaceOverride: overrideIface, }, nil } func (m *MDNSHost) Close() { if m != nil { m.MDNS.Shutdown() } } // Scan with given timeout and domain filter. Use m.Host.Domain for scanning similar typed devices func (m *MDNSHost) Scan(timeout int, domainFilter string) []*NetworkHost { // Discover all services on the network (e.g. _workstation._tcp) var zcoption zeroconf.ClientOption = nil if m.IfaceOverride != nil { zcoption = zeroconf.SelectIfaces([]net.Interface{*m.IfaceOverride}) } resolver, err := zeroconf.NewResolver(zcoption) if err != nil { log.Fatalln("Failed to initialize resolver:", err.Error()) } entries := make(chan *zeroconf.ServiceEntry) //Create go routine to wait for the resolver discoveredHost := []*NetworkHost{} go func(results <-chan *zeroconf.ServiceEntry) { for entry := range results { if domainFilter == "" { //This is a ArOZ Online Host //Split the required information out of the text element TEXT := entry.Text properties := map[string]string{} for _, v := range TEXT { kv := strings.Split(v, "=") if len(kv) == 2 { properties[kv[0]] = kv[1] } } var macAddrs []string val, ok := properties["mac_addr"] if !ok || val == "" { //No MacAddr found. Target node version too old macAddrs = []string{} } else { macAddrs = strings.Split(properties["mac_addr"], ",") } //log.Println(properties) discoveredHost = append(discoveredHost, &NetworkHost{ HostName: entry.HostName, Port: entry.Port, IPv4: entry.AddrIPv4, Domain: properties["domain"], Model: properties["model"], UUID: properties["uuid"], Vendor: properties["vendor"], BuildVersion: properties["version_build"], MacAddr: macAddrs, Online: true, }) } else { if utils.StringInArray(entry.Text, "domain="+domainFilter) { //This is generic scan request //Split the required information out of the text element TEXT := entry.Text properties := map[string]string{} for _, v := range TEXT { kv := strings.Split(v, "=") if len(kv) == 2 { properties[kv[0]] = kv[1] } } var macAddrs []string val, ok := properties["mac_addr"] if !ok || val == "" { //No MacAddr found. Target node version too old macAddrs = []string{} } else { macAddrs = strings.Split(properties["mac_addr"], ",") } //log.Println(properties) discoveredHost = append(discoveredHost, &NetworkHost{ HostName: entry.HostName, Port: entry.Port, IPv4: entry.AddrIPv4, Domain: properties["domain"], Model: properties["model"], UUID: properties["uuid"], Vendor: properties["vendor"], BuildVersion: properties["version_build"], MacAddr: macAddrs, Online: true, }) } } } }(entries) //Resolve each of the mDNS and pipe it back to the log functions ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(timeout)) defer cancel() err = resolver.Browse(ctx, "_http._tcp", "local.", entries) if err != nil { log.Fatalln("Failed to browse:", err.Error()) } //Update the master scan record <-ctx.Done() return discoveredHost } // Get all mac address of all interfaces func getMacAddr() ([]string, error) { ifas, err := net.Interfaces() if err != nil { return nil, err } var as []string for _, ifa := range ifas { a := ifa.HardwareAddr.String() if a != "" { as = append(as, a) } } return as, nil }