diff --git a/server.go b/server.go index bd74262..a681116 100644 --- a/server.go +++ b/server.go @@ -21,6 +21,8 @@ const ( var defaultTTL uint32 = 3200 +var hostnameFunc = os.Hostname + type serverOpts struct { ttl uint32 connFactory api.ConnectionFactory @@ -88,13 +90,21 @@ func Register(instance, service, domain string, port int, text []string, ifaces var err error if entry.HostName == "" { - entry.HostName, err = os.Hostname() + entry.HostName, err = hostnameFunc() if err != nil { return nil, fmt.Errorf("could not determine host") } } - if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) { + // On MacOS os.Hostname() returns the hostname with the domain at the end but without trailing "." + // e.g. "MacBook-Air.local" in this case we simply add a dot to get a fully qualified mdns domain + if strings.HasSuffix(entry.HostName, trimDot(entry.Domain)) { + entry.HostName += "." + } + + // Ensure domain has trailing dot + entry.Domain = fmt.Sprintf("%s.", trimDot(entry.Domain)) + if !strings.HasSuffix(entry.HostName, entry.Domain) { entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain)) } @@ -147,7 +157,15 @@ func RegisterProxy(instance, service, domain string, port int, host string, ips return nil, fmt.Errorf("missing port") } - if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) { + // On MacOS os.Hostname() returns the hostname with the domain at the end but without trailing "." + // e.g. "MacBook-Air.local" in this case we simply add a dot to get a fully qualified mdns domain + if strings.HasSuffix(entry.HostName, trimDot(entry.Domain)) { + entry.HostName += "." + } + + // Ensure domain has trailing dot + entry.Domain = fmt.Sprintf("%s.", trimDot(entry.Domain)) + if !strings.HasSuffix(entry.HostName, entry.Domain) { entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain)) } diff --git a/service_test.go b/service_test.go index d2f39b4..a31aba6 100644 --- a/service_test.go +++ b/service_test.go @@ -2,6 +2,7 @@ package zeroconf import ( "context" + "fmt" "log" "testing" "time" @@ -202,3 +203,162 @@ func TestSubtype(t *testing.T) { } }) } + +func TestFullyQualifiedDomain(t *testing.T) { + discoverEntry := func(t *testing.T, instance, service, domain string) *ServiceEntry { + t.Helper() + + time.Sleep(time.Second) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + entries := make(chan *ServiceEntry, 100) + expired := make(chan *ServiceEntry, 100) + if err := Browse(ctx, service, fmt.Sprintf("%s.", trimDot(domain)), entries, expired); err != nil { + t.Fatalf("Expected browse success, but got %v", err) + } + <-ctx.Done() + + if len(entries) == 0 { + t.Fatal("Expected at least one service entry, but got none") + } + + for len(entries) > 0 { + result := <-entries + if result.Instance == instance { + return result + } + } + + t.Fatalf("Expected service entry for instance %q, but did not find it", instance) + return nil + } + + t.Run("Register", func(t *testing.T) { + testCases := []struct { + name string + hostName string + domain string + expectedHost string + }{ + { + name: "short hostname without trailing dot in domain", + hostName: "Laptop-1", + domain: "local", + expectedHost: "Laptop-1.local.", + }, + { + name: "short hostname with trailing dot in domain", + hostName: "Laptop-1", + domain: "local.", + expectedHost: "Laptop-1.local.", + }, + { + name: "hostname including domain without trailing dot in domain", + hostName: "MacBook-Air.local", + domain: "local", + expectedHost: "MacBook-Air.local.", + }, + { + name: "hostname including domain with trailing dot in domain", + hostName: "MacBook-Air.local", + domain: "local.", + expectedHost: "MacBook-Air.local.", + }, + } + + for i, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + origHostnameFunc := hostnameFunc + hostnameFunc = func() (string, error) { + return tc.hostName, nil + } + t.Cleanup(func() { + hostnameFunc = origHostnameFunc + }) + + instance := fmt.Sprintf("test-register-fqdn-%d", i) + service := fmt.Sprintf("_fqdn-register-%d._tcp", i) + + server, err := Register(instance, service, tc.domain, mdnsPort, []string{"txtv=0"}, nil) + if err != nil { + t.Fatalf("error while registering mdns service: %s", err) + } + t.Cleanup(server.Shutdown) + + result := discoverEntry(t, instance, service, tc.domain) + if result.Domain != "local." { + t.Fatalf("Expected domain is local., but got %s", result.Domain) + } + if result.HostName != tc.expectedHost { + t.Fatalf("Expected hostname is %s, but got %s", tc.expectedHost, result.HostName) + } + }) + } + }) + + t.Run("RegisterProxy", func(t *testing.T) { + testCases := []struct { + name string + hostName string + domain string + expectedHost string + }{ + { + name: "short hostname without trailing dot in domain", + hostName: "Laptop-1", + domain: "local", + expectedHost: "Laptop-1.local.", + }, + { + name: "short hostname with trailing dot in domain", + hostName: "Laptop-1", + domain: "local.", + expectedHost: "Laptop-1.local.", + }, + { + name: "hostname including domain without trailing dot in domain", + hostName: "MacBook-Air.local", + domain: "local", + expectedHost: "MacBook-Air.local.", + }, + { + name: "hostname including domain with trailing dot in domain", + hostName: "MacBook-Air.local", + domain: "local.", + expectedHost: "MacBook-Air.local.", + }, + } + + for i, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + instance := fmt.Sprintf("test-registerproxy-fqdn-%d", i) + service := fmt.Sprintf("_fqdn-registerproxy-%d._tcp", i) + + server, err := RegisterProxy( + instance, + service, + tc.domain, + mdnsPort, + tc.hostName, + []string{"192.168.1.100"}, + []string{"txtv=0"}, + nil, + ) + if err != nil { + t.Fatalf("error while registering proxy mdns service: %s", err) + } + t.Cleanup(server.Shutdown) + + result := discoverEntry(t, instance, service, tc.domain) + if result.Domain != "local." { + t.Fatalf("Expected domain is local., but got %s", result.Domain) + } + if result.HostName != tc.expectedHost { + t.Fatalf("Expected hostname is %s, but got %s", tc.expectedHost, result.HostName) + } + }) + } + }) +}