package main import ( "strings" "testing" ) const browse = `+;enp6s0;IPv4;home\032server;_ssh._tcp;local +;enp6s0;IPv4;Printer\046Co;_ipp._tcp;local =;enp6s0;IPv4;home\032server;_ssh._tcp;local;home-server.local;192.168.1.10;22; =;enp6s0;IPv4;Printer\046Co;_ipp._tcp;local;printer.local;192.168.1.20;631;"txtvers=1" "rp=ipp/print" +;enp6s0;IPv6;Kitchen;_spotify-connect._tcp;local ` func TestABrowseIsReadResolvedOnceAndUnescaped(t *testing.T) { s := ParseBrowse(browse) if len(s) != 3 { t.Fatalf("%+v", s) } by := map[string]Service{} for _, x := range s { by[x.Name] = x } ssh := by["home server"] if !ssh.Resolved || ssh.Address != "192.168.1.10" || ssh.Port != 22 || ssh.Host != "home-server.local" { t.Fatalf("%+v", ssh) } ipp := by["Printer.Co"] if strings.Join(ipp.TXT, ",") != "txtvers=1,rp=ipp/print" { t.Fatalf("%+v", ipp) } if k := by["Kitchen"]; k.Resolved || k.Type != "_spotify-connect._tcp" { t.Fatalf("%+v", k) } if unescape(`caf\195\169`) != "café" || unescape(`a\.b`) != "a.b" { t.Fatal("unescape") } } func TestABrowseThatHearsNothingSaysTheFilterDropsMDNS(t *testing.T) { var calls []call m := machine(fake(func(c call) Ran { switch c.String() { case "timeout 5 avahi-browse -p -r -t -a": return Ran{Status: 124} case "sudo -n nft list ruleset": return Ran{Stdout: "table inet mesh {\n chain input {\n type filter hook input priority filter; policy drop;\n tcp dport 22 accept\n }\n}\n"} } return Ran{Status: 99} }, &calls), 1000) r, err := m.Browse(5, "") if err != nil || r["count"] != 0 || !strings.Contains(r["note"].(string), "drops inbound UDP 5353") { t.Fatalf("%v %v", r, err) } if _, err := m.Browse(5, "ssh; rm"); err == nil { t.Fatal("not a service type") } } func TestTheFilterIsReadForAnAcceptedInboundMDNS(t *testing.T) { for rs, want := range map[string]bool{ "\t\tudp dport 5353 accept\n": true, "\t\tiifname \"enp6s0\" udp dport { 53, 5353 } accept\n": true, "\t\tudp dport mdns accept\n": true, "\t\tudp dport 53 accept\n": false, "\t\tudp dport 5353 drop\n": false, "\t\tip saddr 10.0.0.0/8 udp dport 15353 accept\n": false, } { if InboundMDNS(rs) != want { t.Errorf("%q: %v", rs, !want) } } } func TestStatusNamesTheSwitchTheFilterAndTheDaemon(t *testing.T) { m := machine(fake(func(c call) Ran { switch { case c.name == "systemctl" && c.args[1] == Daemon: return Ran{Stdout: "LoadState=loaded\nActiveState=active\nUnitFileState=enabled\n"} case c.name == "systemctl": return Ran{Stdout: "ActiveState=inactive\n"} case c.String() == "avahi-daemon --version": return Ran{Stdout: "avahi-daemon 0.9-rc5\n"} case c.String() == "pacman -Q nss-mdns": return Ran{Stdout: "nss-mdns 0.15.1-2\n"} case c.String() == "sudo -n nft list ruleset": return Ran{Stdout: "udp dport 53 accept\n"} } return Ran{Status: 99} }, nil), 1000) files := map[string]string{ DaemonConf: "[server]\nuse-ipv4=yes\n#host-name=foo\nallow-interfaces=enp6s0\n[publish]\npublish-hinfo=no\n", NSSwitch: "passwd: files\nhosts: mymachines files dns mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return]\n", } m.ReadFile = func(p string) ([]byte, error) { if s, ok := files[p]; ok { return []byte(s), nil } return nil, errNoFile } s, err := m.GetStatus() if err != nil { t.Fatal(err) } if !s.MDNSWired || s.NSSMDNS != "nss-mdns 0.15.1-2" || s.InboundMDNS == nil || *s.InboundMDNS || s.Version != "avahi-daemon 0.9-rc5" { t.Fatalf("%+v", s) } if s.Config["server"]["allow-interfaces"] != "enp6s0" || s.Config["server"]["host-name"] != "" || s.Daemon["ActiveState"] != "active" { t.Fatalf("%+v", s.Config) } if len(s.Notes) != 1 || !strings.Contains(s.Notes[0], "drops inbound UDP 5353") { t.Fatalf("%v", s.Notes) } if _, wired := HostsLine("hosts: files dns\n"); wired { t.Fatal("no mdns on the line") } } func TestResolveAsksAvahiAndTheNameServiceAndReadsAFailureFromStderr(t *testing.T) { m := machine(byLine(map[string]Ran{ "avahi-resolve -n printer.local": {Stdout: "printer.local\t192.168.1.20\n"}, "getent hosts printer.local": {Status: 2}, "avahi-resolve -n nowhere.local": {Stderr: "Failed to resolve host name 'nowhere.local': Timeout reached\n"}, "getent hosts nowhere.local": {Status: 2}, "sudo -n nft list ruleset": {Stdout: "udp dport 5353 accept\n"}, "avahi-resolve -a 192.168.1.20": {Stdout: "192.168.1.20\tprinter.local\n"}, }, nil), 1000) r, err := m.Resolve("printer", "") if err != nil { t.Fatal(err) } if r["avahi"].(map[string]any)["resolved"] != true || r["name_service"].(map[string]any)["resolved"] != false { t.Fatalf("%v", r) } r, _ = m.Resolve("nowhere.local", "") if a := r["avahi"].(map[string]any); a["resolved"] != false || !strings.Contains(a["error"].(string), "Timeout reached") || r["note"] != "nothing was heard on the local network" { t.Fatalf("%v", r) } r, _ = m.Resolve("", "192.168.1.20") if r["avahi"].(map[string]any)["answers"].([]string)[0] != "printer.local" { t.Fatalf("%v", r) } for _, bad := range [][2]string{{"", ""}, {"a", "1.2.3.4"}, {"", "not-an-ip"}} { if _, err := m.Resolve(bad[0], bad[1]); err == nil { t.Errorf("%v accepted", bad) } } } func TestPublishedServicesAreReadFromTheirFiles(t *testing.T) { m := machine(byLine(map[string]Ran{ "find /etc/avahi/services -mindepth 1 -maxdepth 1 -name *.service -printf %f\n": {Stdout: "ssh.service\n"}, }, nil), 1000) m.ReadFile = func(string) ([]byte, error) { return []byte(`%h_ssh._tcp22`), nil } r, err := m.Services() if err != nil { t.Fatal(err) } p := r["published"].([]Published) if len(p) != 1 || p[0].Name != "%h" || p[0].Types[0] != "_ssh._tcp" || p[0].Ports[0] != 22 { t.Fatalf("%+v", p) } }