├── .editorconfig ├── LICENSE ├── README.md ├── address-resolver.go ├── domain-browser.go ├── entry-group.go ├── go.mod ├── go.sum ├── host-name-resolver.go ├── record-browser.go ├── server.go ├── server_test.go ├── service-browser.go ├── service-resolver.go ├── service-type-browser.go ├── signal-emitter.go └── types.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = tab 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2019 Holoplot GmbH 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang bindings for Avahi 2 | 3 | Avahi is an implementation of the mDNS protocol. Refer to the [Wikipedia article](https://en.wikipedia.org/wiki/Avahi_(software)), 4 | the [website](https://www.avahi.org/) and the [GitHub project](https://github.com/lathiat/avahi) for further information. 5 | 6 | This Go package provides bindings for DBus interfaces exposed by the Avahi daemon. 7 | 8 | # Install 9 | 10 | Install the package like this: 11 | 12 | ``` 13 | go get https://github.com/holoplot/go-avahi 14 | ``` 15 | 16 | And then use it in your source code. 17 | 18 | ``` 19 | import "github.com/holoplot/go-avahi" 20 | ``` 21 | 22 | # Examples 23 | 24 | Below are some examples to illustrate the usage of this package. 25 | Note that you will need to have a working Avahi installation. 26 | 27 | ## Browsing and resolving 28 | 29 | ```go 30 | package main 31 | 32 | import ( 33 | "log" 34 | 35 | "github.com/godbus/dbus/v5" 36 | "github.com/holoplot/go-avahi" 37 | ) 38 | 39 | func main() { 40 | conn, err := dbus.SystemBus() 41 | if err != nil { 42 | log.Fatalf("Cannot get system bus: %v", err) 43 | } 44 | 45 | // This is a globally shared connection. If there are other users of DBUS (whether in this library or others, 46 | // this shouldn't be called until all users are done. 47 | defer conn.Close() 48 | 49 | server, err := avahi.ServerNew(conn) 50 | if err != nil { 51 | log.Fatalf("Avahi new failed: %v", err) 52 | } 53 | 54 | defer server.Close() 55 | 56 | host, err := server.GetHostName() 57 | if err != nil { 58 | log.Fatalf("GetHostName() failed: %v", err) 59 | } 60 | log.Println("GetHostName()", host) 61 | 62 | fqdn, err := server.GetHostNameFqdn() 63 | if err != nil { 64 | log.Fatalf("GetHostNameFqdn() failed: %v", err) 65 | } 66 | log.Println("GetHostNameFqdn()", fqdn) 67 | 68 | s, err := server.GetAlternativeHostName(host) 69 | if err != nil { 70 | log.Fatalf("GetAlternativeHostName() failed: %v", err) 71 | } 72 | log.Println("GetAlternativeHostName()", s) 73 | 74 | i, err := server.GetAPIVersion() 75 | if err != nil { 76 | log.Fatalf("GetAPIVersion() failed: %v", err) 77 | } 78 | log.Println("GetAPIVersion()", i) 79 | 80 | hn, err := server.ResolveHostName(avahi.InterfaceUnspec, avahi.ProtoUnspec, fqdn, avahi.ProtoUnspec, 0) 81 | if err != nil { 82 | log.Fatalf("ResolveHostName() failed: %v", err) 83 | } 84 | log.Println("ResolveHostName:", hn) 85 | 86 | db, err := server.DomainBrowserNew(avahi.InterfaceUnspec, avahi.ProtoUnspec, "", avahi.DomainBrowserTypeBrowseDefault, 0) 87 | if err != nil { 88 | log.Fatalf("DomainBrowserNew() failed: %v", err) 89 | } 90 | 91 | stb, err := server.ServiceTypeBrowserNew(avahi.InterfaceUnspec, avahi.ProtoUnspec, "local", 0) 92 | if err != nil { 93 | log.Fatalf("ServiceTypeBrowserNew() failed: %v", err) 94 | } 95 | 96 | sb, err := server.ServiceBrowserNew(avahi.InterfaceUnspec, avahi.ProtoUnspec, "_my-nifty-service._tcp", "local", 0) 97 | if err != nil { 98 | log.Fatalf("ServiceBrowserNew() failed: %v", err) 99 | } 100 | 101 | sr, err := server.ServiceResolverNew(avahi.InterfaceUnspec, avahi.ProtoUnspec, "", "_my-nifty-service._tcp", "local", avahi.ProtoUnspec, 0) 102 | if err != nil { 103 | log.Fatalf("ServiceResolverNew() failed: %v", err) 104 | } 105 | 106 | var domain avahi.Domain 107 | var service avahi.Service 108 | var serviceType avahi.ServiceType 109 | 110 | for { 111 | select { 112 | case domain = <-db.AddChannel: 113 | log.Println("DomainBrowser ADD: ", domain) 114 | case domain = <-db.RemoveChannel: 115 | log.Println("DomainBrowser REMOVE: ", domain) 116 | case serviceType = <-stb.AddChannel: 117 | log.Println("ServiceTypeBrowser ADD: ", serviceType) 118 | case serviceType = <-stb.RemoveChannel: 119 | log.Println("ServiceTypeBrowser REMOVE: ", serviceType) 120 | case service = <-sb.AddChannel: 121 | log.Println("ServiceBrowser ADD: ", service) 122 | 123 | service, err := server.ResolveService(service.Interface, service.Protocol, service.Name, 124 | service.Type, service.Domain, avahi.ProtoUnspec, 0) 125 | if err == nil { 126 | log.Println(" RESOLVED >>", service.Address) 127 | } 128 | case service = <-sb.RemoveChannel: 129 | log.Println("ServiceBrowser REMOVE: ", service) 130 | case service = <-sr.FoundChannel: 131 | log.Println("ServiceResolver FOUND: ", service) 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | ## Publishing 138 | 139 | ```go 140 | package main 141 | 142 | import ( 143 | "log" 144 | 145 | "github.com/godbus/dbus/v5" 146 | "github.com/holoplot/go-avahi" 147 | ) 148 | 149 | func main() { 150 | conn, err := dbus.SystemBus() 151 | if err != nil { 152 | log.Fatalf("Cannot get system bus: %v", err) 153 | } 154 | 155 | a, err := avahi.ServerNew(conn) 156 | if err != nil { 157 | log.Fatalf("Avahi new failed: %v", err) 158 | } 159 | 160 | eg, err := a.EntryGroupNew() 161 | if err != nil { 162 | log.Fatalf("EntryGroupNew() failed: %v", err) 163 | } 164 | 165 | hostname, err := a.GetHostName() 166 | if err != nil { 167 | log.Fatalf("GetHostName() failed: %v", err) 168 | } 169 | 170 | fqdn, err := a.GetHostNameFqdn() 171 | if err != nil { 172 | log.Fatalf("GetHostNameFqdn() failed: %v", err) 173 | } 174 | 175 | err = eg.AddService(avahi.InterfaceUnspec, avahi.ProtoUnspec, 0, hostname, "_my-nifty-service._tcp", "local", fqdn, 1234, nil) 176 | if err != nil { 177 | log.Fatalf("AddService() failed: %v", err) 178 | } 179 | 180 | err = eg.Commit() 181 | if err != nil { 182 | log.Fatalf("Commit() failed: %v", err) 183 | } 184 | 185 | log.Println("Entry published. Hit ^C to exit.") 186 | 187 | for { 188 | select {} 189 | } 190 | } 191 | ``` 192 | 193 | # MIT License 194 | 195 | See file `LICENSE` for details. 196 | -------------------------------------------------------------------------------- /address-resolver.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | 6 | dbus "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | // An AddressResolver resolves Address to IP addresses 10 | type AddressResolver struct { 11 | object dbus.BusObject 12 | FoundChannel chan Address 13 | closeCh chan struct{} 14 | } 15 | 16 | // AddressResolverNew creates a new AddressResolver 17 | func AddressResolverNew(conn *dbus.Conn, path dbus.ObjectPath) (*AddressResolver, error) { 18 | c := new(AddressResolver) 19 | 20 | c.object = conn.Object("org.freedesktop.Avahi", path) 21 | c.FoundChannel = make(chan Address) 22 | c.closeCh = make(chan struct{}) 23 | 24 | return c, nil 25 | } 26 | 27 | func (c *AddressResolver) interfaceForMember(method string) string { 28 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.AddressResolver", method) 29 | } 30 | 31 | func (c *AddressResolver) free() { 32 | if c.closeCh != nil { 33 | close(c.closeCh) 34 | } 35 | c.object.Call(c.interfaceForMember("Free"), 0) 36 | } 37 | 38 | func (c *AddressResolver) getObjectPath() dbus.ObjectPath { 39 | return c.object.Path() 40 | } 41 | 42 | func (c *AddressResolver) dispatchSignal(signal *dbus.Signal) error { 43 | if signal.Name == c.interfaceForMember("Found") { 44 | var address Address 45 | err := dbus.Store(signal.Body, &address.Interface, &address.Protocol, 46 | &address.Aprotocol, &address.Address, &address.Name, 47 | &address.Flags) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | select { 53 | case c.FoundChannel <- address: 54 | case <-c.closeCh: 55 | close(c.FoundChannel) 56 | c.closeCh = nil 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /domain-browser.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | 6 | dbus "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | // A DomainBrowser is used to browse for mDNS domains 10 | type DomainBrowser struct { 11 | object dbus.BusObject 12 | AddChannel chan Domain 13 | RemoveChannel chan Domain 14 | closeCh chan struct{} 15 | } 16 | 17 | const ( 18 | // DomainBrowserTypeBrowse - Browse for a list of available browsing domains 19 | DomainBrowserTypeBrowse = 0 20 | // DomainBrowserTypeBrowseDefault - Browse for the default browsing domain 21 | DomainBrowserTypeBrowseDefault = 1 22 | // DomainBrowserTypeRegister - Browse for a list of available registering domains 23 | DomainBrowserTypeRegister = 2 24 | // DomainBrowserTypeRegisterDefault - Browse for the default registering domain 25 | DomainBrowserTypeRegisterDefault = 3 26 | // DomainBrowserTypeBrowseLegacy - Legacy browse domain - see DNS-SD spec for more information 27 | DomainBrowserTypeBrowseLegacy = 4 28 | ) 29 | 30 | // DomainBrowserNew returns a new domain browser 31 | func DomainBrowserNew(conn *dbus.Conn, path dbus.ObjectPath) (*DomainBrowser, error) { 32 | c := new(DomainBrowser) 33 | 34 | c.object = conn.Object("org.freedesktop.Avahi", path) 35 | c.AddChannel = make(chan Domain) 36 | c.RemoveChannel = make(chan Domain) 37 | c.closeCh = make(chan struct{}) 38 | 39 | return c, nil 40 | } 41 | 42 | func (c *DomainBrowser) interfaceForMember(method string) string { 43 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.DomainBrowser", method) 44 | } 45 | 46 | func (c *DomainBrowser) free() { 47 | if c.closeCh != nil { 48 | close(c.closeCh) 49 | } 50 | c.object.Call(c.interfaceForMember("Free"), 0) 51 | } 52 | 53 | func (c *DomainBrowser) getObjectPath() dbus.ObjectPath { 54 | return c.object.Path() 55 | } 56 | 57 | func (c *DomainBrowser) dispatchSignal(signal *dbus.Signal) error { 58 | if signal.Name == c.interfaceForMember("ItemNew") || signal.Name == c.interfaceForMember("ItemRemove") { 59 | var domain Domain 60 | err := dbus.Store(signal.Body, &domain.Interface, &domain.Protocol, &domain.Domain, &domain.Flags) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | if signal.Name == c.interfaceForMember("ItemNew") { 66 | select { 67 | case c.AddChannel <- domain: 68 | case <-c.closeCh: 69 | close(c.AddChannel) 70 | close(c.RemoveChannel) 71 | c.closeCh = nil 72 | } 73 | } else { 74 | select { 75 | case c.RemoveChannel <- domain: 76 | case <-c.closeCh: 77 | close(c.AddChannel) 78 | close(c.RemoveChannel) 79 | c.closeCh = nil 80 | } 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /entry-group.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | 6 | dbus "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | const ( 10 | // EntryGroupUncommited - The group has not yet been commited, the user must still call Commit() 11 | EntryGroupUncommited = 0 12 | // EntryGroupRegistering - The entries of the group are currently being registered 13 | EntryGroupRegistering = 1 14 | // EntryGroupEstablished - The entries have successfully been established 15 | EntryGroupEstablished = 2 16 | // EntryGroupCollision - A name collision for one of the entries in the group has been detected, the entries have been withdrawn 17 | EntryGroupCollision = 3 18 | // EntryGroupFailure - Some kind of failure happened, the entries have been withdrawn 19 | EntryGroupFailure = 4 20 | ) 21 | 22 | // An EntryGroupState describes the current state of an entry group 23 | type EntryGroupState struct { 24 | State int32 25 | Error string 26 | } 27 | 28 | // An EntryGroup describes a group of records for services 29 | type EntryGroup struct { 30 | conn *dbus.Conn 31 | object dbus.BusObject 32 | StateChangeChannel chan EntryGroupState 33 | } 34 | 35 | // EntryGroupNew creates a new entry group 36 | func EntryGroupNew(conn *dbus.Conn, path dbus.ObjectPath) (*EntryGroup, error) { 37 | c := new(EntryGroup) 38 | c.conn = conn 39 | c.object = c.conn.Object("org.freedesktop.Avahi", path) 40 | c.StateChangeChannel = make(chan EntryGroupState, 10) 41 | 42 | return c, nil 43 | } 44 | 45 | func (c *EntryGroup) interfaceForMember(method string) string { 46 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.EntryGroup", method) 47 | } 48 | 49 | // Commit an AvahiEntryGroup. The entries in the entry group are now registered on the network. 50 | // Commiting empty entry groups is considered an error. 51 | func (c *EntryGroup) Commit() error { 52 | return c.object.Call(c.interfaceForMember("Commit"), 0).Err 53 | } 54 | 55 | // Reset an AvahiEntryGroup. This takes effect immediately. 56 | func (c *EntryGroup) Reset() error { 57 | return c.object.Call(c.interfaceForMember("Reset"), 0).Err 58 | } 59 | 60 | // GetState gets an AvahiEntryGroup's state 61 | func (c *EntryGroup) GetState() (int32, error) { 62 | var i int32 63 | 64 | err := c.object.Call(c.interfaceForMember("GetState"), 0).Store(&i) 65 | if err != nil { 66 | return 0, err 67 | } 68 | 69 | return i, nil 70 | } 71 | 72 | // IsEmpty checks if an AvahiEntryGroup is empty 73 | func (c *EntryGroup) IsEmpty() (bool, error) { 74 | var b bool 75 | 76 | err := c.object.Call(c.interfaceForMember("IsEmpty"), 0).Store(&b) 77 | if err != nil { 78 | return false, err 79 | } 80 | 81 | return b, nil 82 | } 83 | 84 | // AddService adds a service. Takes a list of TXT record strings as last arguments. 85 | // Please note that this service is not announced on the network before Commit() is called. 86 | func (c *EntryGroup) AddService(iface, protocol int32, flags uint32, name, serviceType, domain, host string, port uint16, txt [][]byte) error { 87 | return c.object.Call(c.interfaceForMember("AddService"), 0, iface, protocol, flags, name, serviceType, domain, host, port, txt).Err 88 | } 89 | 90 | // AddServiceSubtype adds a subtype for a service. The service should already be existent in the entry group. 91 | // You may add as many subtypes for a service as you wish. 92 | func (c *EntryGroup) AddServiceSubtype(iface, protocol int32, flags uint32, name, serviceType, domain, subtype string) error { 93 | return c.object.Call(c.interfaceForMember("AddServiceSubtype"), 0, iface, protocol, flags, name, serviceType, domain, subtype).Err 94 | } 95 | 96 | // UpdateServiceTxt apdates a TXT record for an existing service. 97 | // The service should already be existent in the entry group. 98 | func (c *EntryGroup) UpdateServiceTxt(iface, protocol int32, flags uint32, name, serviceType, domain string, txt [][]byte) error { 99 | return c.object.Call(c.interfaceForMember("UpdateServiceTxt"), 0, iface, protocol, flags, name, serviceType, domain, txt).Err 100 | } 101 | 102 | // AddAddress add a host/address pair to the entry group 103 | func (c *EntryGroup) AddAddress(iface, protocol int32, flags uint32, name, address string) error { 104 | return c.object.Call(c.interfaceForMember("AddAddress"), 0, iface, protocol, flags, name, address).Err 105 | } 106 | 107 | // AddRecord adds an arbitrary record. I hope you know what you do. 108 | func (c *EntryGroup) AddRecord(iface, protocol int32, flags uint32, name string, class, recordType uint16, ttl uint32, rdata []byte) error { 109 | return c.object.Call(c.interfaceForMember("AddRecord"), 0, iface, protocol, flags, name, class, recordType, ttl, rdata).Err 110 | } 111 | 112 | func (c *EntryGroup) free() { 113 | c.object.Call(c.interfaceForMember("Free"), 0) 114 | } 115 | 116 | func (c *EntryGroup) getObjectPath() dbus.ObjectPath { 117 | return c.object.Path() 118 | } 119 | 120 | func (c *EntryGroup) dispatchSignal(signal *dbus.Signal) error { 121 | if signal.Name == c.interfaceForMember("StateChanged") { 122 | var state EntryGroupState 123 | err := dbus.Store(signal.Body, &state.State, &state.Error) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | c.StateChangeChannel <- state 129 | } 130 | 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/holoplot/go-avahi 2 | 3 | go 1.17 4 | 5 | require github.com/godbus/dbus/v5 v5.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 2 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 3 | -------------------------------------------------------------------------------- /host-name-resolver.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | 6 | dbus "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | // A HostNameResolver can resolve host names 10 | type HostNameResolver struct { 11 | object dbus.BusObject 12 | FoundChannel chan HostName 13 | closeCh chan struct{} 14 | } 15 | 16 | // HostNameResolverNew returns a new HostNameResolver 17 | func HostNameResolverNew(conn *dbus.Conn, path dbus.ObjectPath) (*HostNameResolver, error) { 18 | c := new(HostNameResolver) 19 | 20 | c.object = conn.Object("org.freedesktop.Avahi", path) 21 | c.FoundChannel = make(chan HostName) 22 | c.closeCh = make(chan struct{}) 23 | 24 | return c, nil 25 | } 26 | 27 | func (c *HostNameResolver) interfaceForMember(method string) string { 28 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.HostNameResolver", method) 29 | } 30 | 31 | func (c *HostNameResolver) free() { 32 | if c.closeCh != nil { 33 | close(c.closeCh) 34 | } 35 | c.object.Call(c.interfaceForMember("Free"), 0) 36 | } 37 | 38 | func (c *HostNameResolver) getObjectPath() dbus.ObjectPath { 39 | return c.object.Path() 40 | } 41 | 42 | func (c *HostNameResolver) dispatchSignal(signal *dbus.Signal) error { 43 | if signal.Name == c.interfaceForMember("Found") { 44 | var hostName HostName 45 | err := dbus.Store(signal.Body, &hostName.Interface, &hostName.Protocol, 46 | &hostName.Name, &hostName.Aprotocol, &hostName.Address, 47 | &hostName.Flags) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | select { 53 | case c.FoundChannel <- hostName: 54 | case <-c.closeCh: 55 | close(c.FoundChannel) 56 | c.closeCh = nil 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /record-browser.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | 6 | dbus "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | // A RecordBrowser is a browser for mDNS records 10 | type RecordBrowser struct { 11 | object dbus.BusObject 12 | AddChannel chan Record 13 | RemoveChannel chan Record 14 | closeCh chan struct{} 15 | } 16 | 17 | // RecordBrowserNew creates a new mDNS record browser 18 | func RecordBrowserNew(conn *dbus.Conn, path dbus.ObjectPath) (*RecordBrowser, error) { 19 | c := new(RecordBrowser) 20 | 21 | c.object = conn.Object("org.freedesktop.Avahi", path) 22 | c.AddChannel = make(chan Record) 23 | c.RemoveChannel = make(chan Record) 24 | c.closeCh = make(chan struct{}) 25 | 26 | return c, nil 27 | } 28 | 29 | func (c *RecordBrowser) interfaceForMember(method string) string { 30 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.RecordBrowser", method) 31 | } 32 | 33 | func (c *RecordBrowser) free() { 34 | close(c.closeCh) 35 | close(c.AddChannel) 36 | close(c.RemoveChannel) 37 | c.object.Call(c.interfaceForMember("Free"), 0) 38 | } 39 | 40 | func (c *RecordBrowser) getObjectPath() dbus.ObjectPath { 41 | return c.object.Path() 42 | } 43 | 44 | func (c *RecordBrowser) dispatchSignal(signal *dbus.Signal) error { 45 | if signal.Name == c.interfaceForMember("ItemNew") || signal.Name == c.interfaceForMember("ItemRemove") { 46 | var record Record 47 | err := dbus.Store(signal.Body, &record.Interface, &record.Protocol, &record.Name, 48 | &record.Class, &record.Type, &record.Rdata, &record.Flags) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if signal.Name == c.interfaceForMember("ItemNew") { 54 | select { 55 | case c.AddChannel <- record: 56 | case <-c.closeCh: 57 | } 58 | } else { 59 | select { 60 | case c.RemoveChannel <- record: 61 | case <-c.closeCh: 62 | } 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | dbus "github.com/godbus/dbus/v5" 8 | ) 9 | 10 | const ( 11 | // ServerInvalid - Invalid state (initial) 12 | ServerInvalid = 0 13 | // ServerRegistering - Host RRs are being registered 14 | ServerRegistering = 1 15 | // ServerRunning - All host RRs have been established 16 | ServerRunning = 2 17 | // ServerCollision - There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via SetHostname() 18 | ServerCollision = 3 19 | // ServerFailure - Some fatal failure happened, the server is unable to proceed 20 | ServerFailure = 4 21 | ) 22 | 23 | // A Server is the cental object of an Avahi connection 24 | type Server struct { 25 | conn *dbus.Conn 26 | object dbus.BusObject 27 | signalChannel chan *dbus.Signal 28 | quitChannel chan struct{} 29 | 30 | mutex sync.Mutex 31 | signalEmitters map[dbus.ObjectPath]signalEmitter 32 | } 33 | 34 | // ServerNew returns a new Server object 35 | func ServerNew(conn *dbus.Conn) (*Server, error) { 36 | c := new(Server) 37 | c.conn = conn 38 | c.object = conn.Object("org.freedesktop.Avahi", dbus.ObjectPath("/")) 39 | c.signalChannel = make(chan *dbus.Signal, 10) 40 | c.quitChannel = make(chan struct{}) 41 | 42 | c.conn.Signal(c.signalChannel) 43 | c.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal',interface='org.freedesktop.Avahi'") 44 | 45 | c.signalEmitters = make(map[dbus.ObjectPath]signalEmitter) 46 | 47 | go func() { 48 | for { 49 | select { 50 | case signal, ok := <-c.signalChannel: 51 | if !ok { 52 | continue 53 | } 54 | 55 | c.mutex.Lock() 56 | for path, obj := range c.signalEmitters { 57 | if path == signal.Path { 58 | _ = obj.dispatchSignal(signal) 59 | } 60 | } 61 | c.mutex.Unlock() 62 | 63 | case <-c.quitChannel: 64 | return 65 | } 66 | } 67 | }() 68 | 69 | return c, nil 70 | } 71 | 72 | // Close closes the connection to a server 73 | func (c *Server) Close() { 74 | c.quitChannel <- struct{}{} 75 | 76 | c.mutex.Lock() 77 | defer c.mutex.Unlock() 78 | 79 | for path, obj := range c.signalEmitters { 80 | obj.free() 81 | delete(c.signalEmitters, path) 82 | } 83 | } 84 | 85 | func (c *Server) interfaceForMember(method string) string { 86 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.Server", method) 87 | } 88 | 89 | // EntryGroupNew returns a new and empty EntryGroup 90 | func (c *Server) EntryGroupNew() (*EntryGroup, error) { 91 | var o dbus.ObjectPath 92 | 93 | c.mutex.Lock() 94 | defer c.mutex.Unlock() 95 | 96 | err := c.object.Call(c.interfaceForMember("EntryGroupNew"), 0).Store(&o) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | r, err := EntryGroupNew(c.conn, o) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | c.signalEmitters[o] = r 107 | 108 | return r, nil 109 | } 110 | 111 | // EntryGroupFree frees an entry group and releases its resources on the service 112 | func (c *Server) EntryGroupFree(r *EntryGroup) { 113 | c.signalEmitterFree(r) 114 | } 115 | 116 | // ResolveHostName ... 117 | func (c *Server) ResolveHostName(iface, protocol int32, name string, aprotocol int32, flags uint32) (reply HostName, err error) { 118 | err = c.object.Call(c.interfaceForMember("ResolveHostName"), 0, iface, protocol, name, aprotocol, flags). 119 | Store(&reply.Interface, &reply.Protocol, &reply.Name, &reply.Aprotocol, &reply.Address, &reply.Flags) 120 | return reply, err 121 | } 122 | 123 | // ResolveAddress ... 124 | func (c *Server) ResolveAddress(iface, protocol int32, address string, flags uint32) (reply Address, err error) { 125 | err = c.object.Call(c.interfaceForMember("ResolveAddress"), 0, iface, protocol, address, flags). 126 | Store(&reply.Interface, &reply.Protocol, &reply.Aprotocol, &reply.Address, &reply.Name, &reply.Flags) 127 | return reply, err 128 | } 129 | 130 | // ResolveService ... 131 | func (c *Server) ResolveService(iface, protocol int32, name, serviceType, domain string, aprotocol int32, flags uint32) (reply Service, err error) { 132 | err = c.object.Call(c.interfaceForMember("ResolveService"), 0, iface, protocol, name, serviceType, domain, aprotocol, flags). 133 | Store(&reply.Interface, &reply.Protocol, &reply.Name, &reply.Type, &reply.Domain, 134 | &reply.Host, &reply.Aprotocol, &reply.Address, &reply.Port, &reply.Txt, &reply.Flags) 135 | return reply, err 136 | } 137 | 138 | // DomainBrowserNew ... 139 | func (c *Server) DomainBrowserNew(iface, protocol int32, domain string, btype int32, flags uint32) (*DomainBrowser, error) { 140 | var o dbus.ObjectPath 141 | 142 | err := c.object.Call(c.interfaceForMember("DomainBrowserNew"), 0, iface, protocol, domain, btype, flags).Store(&o) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | r, err := DomainBrowserNew(c.conn, o) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | return r, nil 153 | } 154 | 155 | // DomainBrowserFree ... 156 | func (c *Server) DomainBrowserFree(r *DomainBrowser) { 157 | c.signalEmitterFree(r) 158 | } 159 | 160 | // ServiceTypeBrowserNew ... 161 | func (c *Server) ServiceTypeBrowserNew(iface, protocol int32, domain string, flags uint32) (*ServiceTypeBrowser, error) { 162 | var o dbus.ObjectPath 163 | 164 | c.mutex.Lock() 165 | defer c.mutex.Unlock() 166 | 167 | err := c.object.Call(c.interfaceForMember("ServiceTypeBrowserNew"), 0, iface, protocol, domain, flags).Store(&o) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | r, err := ServiceTypeBrowserNew(c.conn, o) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | c.signalEmitters[o] = r 178 | 179 | return r, nil 180 | } 181 | 182 | // ServiceTypeBrowserFree ... 183 | func (c *Server) ServiceTypeBrowserFree(r *ServiceTypeBrowser) { 184 | c.signalEmitterFree(r) 185 | } 186 | 187 | // ServiceBrowserNew ... 188 | func (c *Server) ServiceBrowserNew(iface, protocol int32, serviceType string, domain string, flags uint32) (*ServiceBrowser, error) { 189 | var o dbus.ObjectPath 190 | 191 | c.mutex.Lock() 192 | defer c.mutex.Unlock() 193 | 194 | err := c.object.Call(c.interfaceForMember("ServiceBrowserNew"), 0, iface, protocol, serviceType, domain, flags).Store(&o) 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | r, err := ServiceBrowserNew(c.conn, o) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | c.signalEmitters[o] = r 205 | 206 | return r, nil 207 | } 208 | 209 | // ServiceBrowserFree ... 210 | func (c *Server) ServiceBrowserFree(r *ServiceBrowser) { 211 | c.signalEmitterFree(r) 212 | } 213 | 214 | // ServiceResolverNew ... 215 | func (c *Server) ServiceResolverNew(iface, protocol int32, name, serviceType, domain string, aprotocol int32, flags uint32) (*ServiceResolver, error) { 216 | var o dbus.ObjectPath 217 | 218 | c.mutex.Lock() 219 | defer c.mutex.Unlock() 220 | 221 | err := c.object.Call(c.interfaceForMember("ServiceResolverNew"), 0, iface, protocol, name, serviceType, domain, aprotocol, flags).Store(&o) 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | r, err := ServiceResolverNew(c.conn, o) 227 | if err != nil { 228 | return nil, err 229 | } 230 | 231 | c.signalEmitters[o] = r 232 | 233 | return r, nil 234 | } 235 | 236 | // ServiceResolverFree ... 237 | func (c *Server) ServiceResolverFree(r *ServiceResolver) { 238 | c.signalEmitterFree(r) 239 | } 240 | 241 | // HostNameResolverNew ... 242 | func (c *Server) HostNameResolverNew(iface, protocol int32, name string, aprotocol int32, flags uint32) (*HostNameResolver, error) { 243 | var o dbus.ObjectPath 244 | 245 | c.mutex.Lock() 246 | defer c.mutex.Unlock() 247 | 248 | err := c.object.Call(c.interfaceForMember("HostNameResolverNew"), 0, iface, protocol, name, aprotocol, flags).Store(&o) 249 | if err != nil { 250 | return nil, err 251 | } 252 | 253 | r, err := HostNameResolverNew(c.conn, o) 254 | if err != nil { 255 | return nil, err 256 | } 257 | 258 | c.signalEmitters[o] = r 259 | 260 | return r, nil 261 | } 262 | 263 | // AddressResolverNew ... 264 | func (c *Server) AddressResolverNew(iface, protocol int32, address string, flags uint32) (*AddressResolver, error) { 265 | var o dbus.ObjectPath 266 | 267 | c.mutex.Lock() 268 | defer c.mutex.Unlock() 269 | 270 | err := c.object.Call(c.interfaceForMember("AddressResolverNew"), 0, iface, protocol, address, flags).Store(&o) 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | r, err := AddressResolverNew(c.conn, o) 276 | if err != nil { 277 | return nil, err 278 | } 279 | 280 | c.signalEmitters[o] = r 281 | 282 | return r, nil 283 | } 284 | 285 | // AddressResolverFree ... 286 | func (c *Server) AddressResolverFree(r *AddressResolver) { 287 | c.signalEmitterFree(r) 288 | } 289 | 290 | // RecordBrowserNew ... 291 | func (c *Server) RecordBrowserNew(iface, protocol int32, name string, class uint16, recordType uint16, flags uint32) (*RecordBrowser, error) { 292 | var o dbus.ObjectPath 293 | 294 | c.mutex.Lock() 295 | defer c.mutex.Unlock() 296 | 297 | err := c.object.Call(c.interfaceForMember("RecordBrowserNew"), 0, iface, protocol, name, class, recordType, flags).Store(&o) 298 | if err != nil { 299 | return nil, err 300 | } 301 | 302 | r, err := RecordBrowserNew(c.conn, o) 303 | if err != nil { 304 | return nil, err 305 | } 306 | 307 | c.signalEmitters[o] = r 308 | 309 | return r, nil 310 | } 311 | 312 | // RecordBrowserFree ... 313 | func (c *Server) RecordBrowserFree(r *RecordBrowser) { 314 | c.signalEmitterFree(r) 315 | } 316 | 317 | // GetAPIVersion ... 318 | func (c *Server) GetAPIVersion() (int32, error) { 319 | var i int32 320 | 321 | err := c.object.Call(c.interfaceForMember("GetAPIVersion"), 0).Store(&i) 322 | if err != nil { 323 | return 0, err 324 | } 325 | 326 | return i, nil 327 | } 328 | 329 | // GetAlternativeHostName ... 330 | func (c *Server) GetAlternativeHostName(name string) (string, error) { 331 | var s string 332 | 333 | err := c.object.Call(c.interfaceForMember("GetAlternativeHostName"), 0, name).Store(&s) 334 | if err != nil { 335 | return "", err 336 | } 337 | 338 | return s, nil 339 | } 340 | 341 | // GetAlternativeServiceName ... 342 | func (c *Server) GetAlternativeServiceName(name string) (string, error) { 343 | var s string 344 | 345 | err := c.object.Call(c.interfaceForMember("GetAlternativeServiceName"), 0, name).Store(&s) 346 | if err != nil { 347 | return "", err 348 | } 349 | 350 | return s, nil 351 | } 352 | 353 | // GetDomainName ... 354 | func (c *Server) GetDomainName() (string, error) { 355 | var s string 356 | 357 | err := c.object.Call(c.interfaceForMember("GetDomainName"), 0).Store(&s) 358 | if err != nil { 359 | return "", err 360 | } 361 | 362 | return s, nil 363 | } 364 | 365 | // GetHostName ... 366 | func (c *Server) GetHostName() (string, error) { 367 | var s string 368 | 369 | err := c.object.Call(c.interfaceForMember("GetHostName"), 0).Store(&s) 370 | if err != nil { 371 | return "", err 372 | } 373 | 374 | return s, nil 375 | } 376 | 377 | // GetHostNameFqdn ... 378 | func (c *Server) GetHostNameFqdn() (string, error) { 379 | var s string 380 | 381 | err := c.object.Call(c.interfaceForMember("GetHostNameFqdn"), 0).Store(&s) 382 | if err != nil { 383 | return "", err 384 | } 385 | 386 | return s, nil 387 | } 388 | 389 | // GetLocalServiceCookie ... 390 | func (c *Server) GetLocalServiceCookie() (int32, error) { 391 | var i int32 392 | 393 | err := c.object.Call(c.interfaceForMember("GetLocalServiceCookie"), 0).Store(&i) 394 | if err != nil { 395 | return 0, err 396 | } 397 | 398 | return i, nil 399 | } 400 | 401 | // GetNetworkInterfaceIndexByName -... 402 | func (c *Server) GetNetworkInterfaceIndexByName(name string) (int32, error) { 403 | var i int32 404 | 405 | err := c.object.Call(c.interfaceForMember("GetNetworkInterfaceIndexByName"), 0, name).Store(&i) 406 | if err != nil { 407 | return 0, err 408 | } 409 | 410 | return i, nil 411 | } 412 | 413 | // GetNetworkInterfaceNameByIndex ... 414 | func (c *Server) GetNetworkInterfaceNameByIndex(index int32) (string, error) { 415 | var s string 416 | 417 | err := c.object.Call(c.interfaceForMember("GetNetworkInterfaceNameByIndex"), 0, index).Store(&s) 418 | if err != nil { 419 | return "", err 420 | } 421 | 422 | return s, nil 423 | } 424 | 425 | // GetState ... 426 | func (c *Server) GetState() (int32, error) { 427 | var i int32 428 | 429 | err := c.object.Call(c.interfaceForMember("GetState"), 0).Store(&i) 430 | if err != nil { 431 | return 0, err 432 | } 433 | 434 | return i, nil 435 | } 436 | 437 | // GetVersionString ... 438 | func (c *Server) GetVersionString() (string, error) { 439 | var s string 440 | 441 | err := c.object.Call(c.interfaceForMember("GetVersionString"), 0).Store(&s) 442 | if err != nil { 443 | return "", err 444 | } 445 | 446 | return s, nil 447 | } 448 | 449 | // IsNSSSupportAvailable ... 450 | func (c *Server) IsNSSSupportAvailable() (bool, error) { 451 | var b bool 452 | 453 | err := c.object.Call(c.interfaceForMember("IsNSSSupportAvailable"), 0).Store(&b) 454 | if err != nil { 455 | return false, err 456 | } 457 | 458 | return b, nil 459 | } 460 | 461 | // SetServerName ... 462 | func (c *Server) SetServerName(name string) error { 463 | return c.object.Call(c.interfaceForMember("SetServerName"), 0, name).Err 464 | } 465 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/godbus/dbus/v5" 8 | ) 9 | 10 | // TestNew ensures that New() works without errors. 11 | func TestNew(t *testing.T) { 12 | conn, err := dbus.SystemBus() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | _, err = ServerNew(conn) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | 23 | func TestNewClose(t *testing.T) { 24 | conn, err := dbus.SystemBus() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | a, err := ServerNew(conn) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | doneChannel := make(chan struct{}) 35 | go func() { 36 | a.Close() 37 | doneChannel <- struct{}{} 38 | }() 39 | 40 | select { 41 | case <-time.After(2 * time.Second): 42 | t.Fatal("Close() is deadlocked") 43 | case <-doneChannel: 44 | } 45 | } 46 | 47 | func TestBasic(t *testing.T) { 48 | conn, err := dbus.SystemBus() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | a, err := ServerNew(conn) 54 | if err != nil { 55 | t.Fatalf("Avahi new failed: %v", err) 56 | } 57 | 58 | s, err := a.GetHostName() 59 | if err != nil { 60 | t.Fatalf("GetHostName() failed: %v", err) 61 | } 62 | t.Log("GetHostName()", s) 63 | 64 | s, err = a.GetAlternativeHostName(s) 65 | if err != nil { 66 | t.Fatalf("GetAlternativeHostName() failed: %v", err) 67 | } 68 | t.Log("GetAlternativeHostName()", s) 69 | 70 | //// 71 | 72 | i, err := a.GetAPIVersion() 73 | if err != nil { 74 | t.Fatalf("GetAPIVersion() failed: %v", err) 75 | } 76 | t.Log("GetAPIVersion()", i) 77 | 78 | s, err = a.GetNetworkInterfaceNameByIndex(1) 79 | if err != nil { 80 | t.Fatalf("GetNetworkInterfaceNameByIndex() failed: %v", err) 81 | } 82 | t.Log("GetNetworkInterfaceNameByIndex()", s) 83 | 84 | i, err = a.GetNetworkInterfaceIndexByName(s) 85 | if err != nil { 86 | t.Fatalf("GetNetworkInterfaceIndexByName() failed: %v", err) 87 | } 88 | if i != 1 { 89 | t.Fatal("GetNetworkInterfaceIndexByName() returned wrong index") 90 | } 91 | t.Log("GetNetworkInterfaceIndexByName()", i) 92 | 93 | /// 94 | 95 | egc, err := a.EntryGroupNew() 96 | if err != nil { 97 | t.Fatalf("EntryGroupNew() failed: %v", err) 98 | } 99 | 100 | b, err := egc.IsEmpty() 101 | if err != nil { 102 | t.Fatalf("egc.IsEmpty() failed: %v", err) 103 | } 104 | if b != true { 105 | t.Fatal("Entry group must initially be empty") 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /service-browser.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | 6 | dbus "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | // A ServiceBrowser browses for mDNS services 10 | type ServiceBrowser struct { 11 | object dbus.BusObject 12 | AddChannel chan Service 13 | RemoveChannel chan Service 14 | closeCh chan struct{} 15 | } 16 | 17 | // ServiceBrowserNew creates a new browser for mDNS records 18 | func ServiceBrowserNew(conn *dbus.Conn, path dbus.ObjectPath) (*ServiceBrowser, error) { 19 | c := new(ServiceBrowser) 20 | 21 | c.object = conn.Object("org.freedesktop.Avahi", path) 22 | c.AddChannel = make(chan Service) 23 | c.RemoveChannel = make(chan Service) 24 | c.closeCh = make(chan struct{}) 25 | 26 | return c, nil 27 | } 28 | 29 | func (c *ServiceBrowser) interfaceForMember(method string) string { 30 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.ServiceBrowser", method) 31 | } 32 | 33 | func (c *ServiceBrowser) free() { 34 | if c.closeCh != nil { 35 | close(c.closeCh) 36 | } 37 | c.object.Call(c.interfaceForMember("Free"), 0) 38 | } 39 | 40 | func (c *ServiceBrowser) getObjectPath() dbus.ObjectPath { 41 | return c.object.Path() 42 | } 43 | 44 | func (c *ServiceBrowser) dispatchSignal(signal *dbus.Signal) error { 45 | if signal.Name == c.interfaceForMember("ItemNew") || signal.Name == c.interfaceForMember("ItemRemove") { 46 | var service Service 47 | err := dbus.Store(signal.Body, &service.Interface, &service.Protocol, &service.Name, &service.Type, &service.Domain, &service.Flags) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if signal.Name == c.interfaceForMember("ItemNew") { 53 | select { 54 | case c.AddChannel <- service: 55 | case <-c.closeCh: 56 | close(c.AddChannel) 57 | close(c.RemoveChannel) 58 | c.closeCh = nil 59 | } 60 | } else { 61 | select { 62 | case c.RemoveChannel <- service: 63 | case <-c.closeCh: 64 | close(c.AddChannel) 65 | close(c.RemoveChannel) 66 | c.closeCh = nil 67 | } 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /service-resolver.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | 6 | dbus "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | // A ServiceResolver resolves mDNS services to IP addresses 10 | type ServiceResolver struct { 11 | object dbus.BusObject 12 | FoundChannel chan Service 13 | closeCh chan struct{} 14 | } 15 | 16 | // ServiceResolverNew returns a new mDNS service resolver 17 | func ServiceResolverNew(conn *dbus.Conn, path dbus.ObjectPath) (*ServiceResolver, error) { 18 | c := new(ServiceResolver) 19 | 20 | c.object = conn.Object("org.freedesktop.Avahi", path) 21 | c.FoundChannel = make(chan Service) 22 | c.closeCh = make(chan struct{}) 23 | 24 | return c, nil 25 | } 26 | 27 | func (c *ServiceResolver) interfaceForMember(method string) string { 28 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.ServiceResolver", method) 29 | } 30 | 31 | func (c *ServiceResolver) free() { 32 | if c.closeCh != nil { 33 | close(c.closeCh) 34 | } 35 | c.object.Call(c.interfaceForMember("Free"), 0) 36 | } 37 | 38 | func (c *ServiceResolver) getObjectPath() dbus.ObjectPath { 39 | return c.object.Path() 40 | } 41 | 42 | func (c *ServiceResolver) dispatchSignal(signal *dbus.Signal) error { 43 | if signal.Name == c.interfaceForMember("Found") { 44 | var service Service 45 | err := dbus.Store(signal.Body, &service.Interface, &service.Protocol, 46 | &service.Name, &service.Type, &service.Domain, &service.Host, 47 | &service.Aprotocol, &service.Address, &service.Port, 48 | &service.Txt, &service.Flags) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | select { 54 | case c.FoundChannel <- service: 55 | case <-c.closeCh: 56 | close(c.FoundChannel) 57 | c.closeCh = nil 58 | } 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /service-type-browser.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import ( 4 | "fmt" 5 | 6 | dbus "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | // A ServiceTypeBrowser is used to browser the mDNS network for services of a specific type 10 | type ServiceTypeBrowser struct { 11 | object dbus.BusObject 12 | AddChannel chan ServiceType 13 | RemoveChannel chan ServiceType 14 | closeCh chan struct{} 15 | } 16 | 17 | // ServiceTypeBrowserNew creates a new browser for mDNS service types 18 | func ServiceTypeBrowserNew(conn *dbus.Conn, path dbus.ObjectPath) (*ServiceTypeBrowser, error) { 19 | c := new(ServiceTypeBrowser) 20 | 21 | c.object = conn.Object("org.freedesktop.Avahi", path) 22 | c.AddChannel = make(chan ServiceType) 23 | c.RemoveChannel = make(chan ServiceType) 24 | c.closeCh = make(chan struct{}) 25 | 26 | return c, nil 27 | } 28 | 29 | func (c *ServiceTypeBrowser) interfaceForMember(method string) string { 30 | return fmt.Sprintf("%s.%s", "org.freedesktop.Avahi.ServiceTypeBrowser", method) 31 | } 32 | 33 | func (c *ServiceTypeBrowser) free() { 34 | if c.closeCh != nil { 35 | close(c.closeCh) 36 | } 37 | c.object.Call(c.interfaceForMember("Free"), 0) 38 | } 39 | 40 | func (c *ServiceTypeBrowser) getObjectPath() dbus.ObjectPath { 41 | return c.object.Path() 42 | } 43 | 44 | func (c *ServiceTypeBrowser) dispatchSignal(signal *dbus.Signal) error { 45 | if signal.Name == c.interfaceForMember("ItemNew") || signal.Name == c.interfaceForMember("ItemRemove") { 46 | var serviceType ServiceType 47 | err := dbus.Store(signal.Body, &serviceType.Interface, &serviceType.Protocol, &serviceType.Type, &serviceType.Domain, &serviceType.Flags) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if signal.Name == c.interfaceForMember("ItemNew") { 53 | select { 54 | case c.AddChannel <- serviceType: 55 | case <-c.closeCh: 56 | close(c.AddChannel) 57 | close(c.RemoveChannel) 58 | c.closeCh = nil 59 | } 60 | } else { 61 | select { 62 | case c.RemoveChannel <- serviceType: 63 | case <-c.closeCh: 64 | close(c.AddChannel) 65 | close(c.RemoveChannel) 66 | c.closeCh = nil 67 | } 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /signal-emitter.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | import dbus "github.com/godbus/dbus/v5" 4 | 5 | type signalEmitter interface { 6 | dispatchSignal(signal *dbus.Signal) error 7 | getObjectPath() dbus.ObjectPath 8 | free() 9 | } 10 | 11 | func (c *Server) signalEmitterFree(e signalEmitter) { 12 | o := e.getObjectPath() 13 | 14 | c.mutex.Lock() 15 | defer c.mutex.Unlock() 16 | 17 | _, ok := c.signalEmitters[o] 18 | if ok { 19 | delete(c.signalEmitters, o) 20 | } 21 | 22 | e.free() 23 | } 24 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package avahi 2 | 3 | // Domain ... 4 | type Domain struct { 5 | Interface int32 6 | Protocol int32 7 | Domain string 8 | Flags uint32 9 | } 10 | 11 | // HostName ... 12 | type HostName struct { 13 | Interface int32 14 | Protocol int32 15 | Name string 16 | Aprotocol int32 17 | Address string 18 | Flags uint32 19 | } 20 | 21 | // Address ... 22 | type Address struct { 23 | Interface int32 24 | Protocol int32 25 | Aprotocol int32 26 | Address string 27 | Name string 28 | Flags uint32 29 | } 30 | 31 | // ServiceType ... 32 | type ServiceType struct { 33 | Interface int32 34 | Protocol int32 35 | Type string 36 | Domain string 37 | Flags uint32 38 | } 39 | 40 | // Service ... 41 | type Service struct { 42 | Interface int32 43 | Protocol int32 44 | Name string 45 | Type string 46 | Domain string 47 | Host string 48 | Aprotocol int32 49 | Address string 50 | Port uint16 51 | Txt [][]byte 52 | Flags uint32 53 | } 54 | 55 | // Record ... 56 | type Record struct { 57 | Interface int32 58 | Protocol int32 59 | Name string 60 | Class uint16 61 | Type uint16 62 | Rdata []byte 63 | Flags uint32 64 | } 65 | 66 | const ( 67 | // ProtoInet - IPv4 68 | ProtoInet = 0 69 | // ProtoInet6 - IPv6 70 | ProtoInet6 = 1 71 | // ProtoUnspec - Unspecified/all protocol(s) 72 | ProtoUnspec = -1 73 | ) 74 | 75 | const ( 76 | // InterfaceUnspec - Unspecified/all interface(s) 77 | InterfaceUnspec = -1 78 | ) 79 | 80 | const ( 81 | // PublishUnique - The RRset is intended to be unique 82 | PublishUnique = 1 83 | // PublishNoProbe - Though the RRset is intended to be unique no probes shall be sent 84 | PublishNoProbe = 2 85 | // PublishNoAnnouce - Do not announce this RR to other hosts 86 | PublishNoAnnouce = 4 87 | // PublishAllowMultiple - Allow multiple local records of this type, even if they are intended to be unique 88 | PublishAllowMultiple = 8 89 | // PublishNoReverse - don't create a reverse (PTR) entry 90 | PublishNoReverse = 16 91 | // PublishNoCookie - do not implicitly add the local service cookie to TXT data 92 | PublishNoCookie = 32 93 | // PublishUpdate - Update existing records instead of adding new ones 94 | PublishUpdate = 64 95 | // PublishUseWideArea - Register the record using wide area DNS (i.e. unicast DNS update) 96 | PublishUseWideArea = 128 97 | // PublishUseMulticast - Register the record using multicast DNS 98 | PublishUseMulticast = 256 99 | ) 100 | 101 | const ( 102 | // LookupUseWideArea - Force lookup via wide area DNS 103 | LookupUseWideArea = 1 104 | // LookupUseMulticast - Force lookup via multicast DNS 105 | LookupUseMulticast = 2 106 | // LookupNoTXT - When doing service resolving, don't lookup TXT record 107 | LookupNoTXT = 4 108 | // LookupNoAddreess - When doing service resolving, don't lookup A/AAAA record 109 | LookupNoAddreess = 8 110 | ) 111 | 112 | const ( 113 | // LookupResultCached - This response originates from the cache 114 | LookupResultCached = 1 115 | // LookupResultWideArea - This response originates from wide area DNS 116 | LookupResultWideArea = 2 117 | // LookupResultMulticast - This response originates from multicast DNS 118 | LookupResultMulticast = 4 119 | // LookupResultLocal - This record/service resides on and was announced by the local host. Only available in service and record browsers and only on AVAHI_BROWSER_NEW. 120 | LookupResultLocal = 8 121 | // LookupResultOurOwn - This service belongs to the same local client as the browser object. Only available in avahi-client, and only for service browsers and only on AVAHI_BROWSER_NEW. 122 | LookupResultOurOwn = 16 123 | // LookupResultStatic - The returned data has been defined statically by some configuration option 124 | LookupResultStatic = 32 125 | ) 126 | --------------------------------------------------------------------------------