diff --git a/README.md b/README.md index 6cc3eb0..709eee4 100644 --- a/README.md +++ b/README.md @@ -139,3 +139,161 @@ if watchdog != nil { }() } ``` + +## Resolved + +[![PkgGoDev](https://pkg.go.dev/badge/github.com/iguanesolutions/go-systemd/resolved/resolved)](https://pkg.go.dev/github.com/iguanesolutions/go-systemd/v5/resolved) + +This package is still under development and very experimental, do not use it in production. +We started this package in order to go deep into the DNS world. So we are opened to any suggestions/contributions on this. +DNS is not trivial at all so there can be some stuff that are not rfc compliant (like sorting addresses etc...). + +The resolved package features: + * Pure Go implementation of `org.freedesktop.resolve1` dbus interface + * Resolver type (which uses the underlying dbus interface) that tries to implement the same methods as `net.Resolver` from Go standard library + * Unit tests (make sure Go resolver and systemd-resolved query the same dns server) + +### Dbus + +The following example shows how to use the resolve1 dbus connection to resolve an host: + +```go +package main + +import ( + "context" + "fmt" + "log" + "syscall" + + "github.com/iguanesolutions/go-systemd/v5/resolved" +) + +func main() { + c, err := resolved.NewConn() + if err != nil { + log.Fatal("ERROR: ", err) + } + ctx := context.Background() + addrs, canonical, flags, err := c.ResolveHostname(ctx, 0, "google.com", syscall.AF_UNSPEC, 0) + if err != nil { + log.Println("ERROR: ", err) + } else { + fmt.Println("Addresses: ", addrs) + fmt.Println("Canonical: ", canonical) + fmt.Println("OutputFlags: ", flags) + } + err = c.Close() + if err != nil { + log.Println("ERROR: ", err) + } +} +``` + +Output: + +```output +Addresses: [{ + IfIndex: 2, + Family: 2, + IP: 142.250.74.238, +} { + IfIndex: 2, + Family: 10, + IP: 2a00:1450:4007:80b::200e, +}] +Canonical: google.com +Flags: 1 +``` + +### Resolver + +The following example shows how to use the resolved Resolver to resolve an host: + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/iguanesolutions/go-systemd/v5/resolved" +) + +func main() { + r, err := resolved.NewResolver() + if err != nil { + log.Fatal("ERROR: ", err) + } + ctx := context.Background() + addrs, err := r.LookupHost(ctx, "google.com") + if err != nil { + log.Println("ERROR: ", err) + } else { + fmt.Println("Addresses: ", addrs) + } + err = r.Close() + if err != nil { + log.Println("ERROR: ", err) + } +} +``` + +Output: + +```output +Addresses: [2a00:1450:4007:80b::200e 142.250.74.238] +``` + +### HTTP Client + +The following example shows how to use the systemd-resolved Resolver with the Go http client from the standard library: + +```go +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/iguanesolutions/go-systemd/v5/resolved" +) + +func main() { + r, err := resolved.NewResolver() + if err != nil { + log.Fatal("ERROR: ", err) + } + // if you want to make a custom http client using systemd-resolved as resolver + httpCli := &http.Client{ + Transport: &http.Transport{ + DialContext: r.DialContext, + }, + } + // or if you don't have an http client you can call HTTPClient method on resolver + // it comes with some nice default values. + httpCli = r.HTTPClient() + resp, err := httpCli.Get("https://google.com") + if err != nil { + log.Println("ERROR: ", err) + } else { + fmt.Println("Status: ", resp.Status) + err = resp.Body.Close() + if err != nil { + log.Println("ERROR: ", err) + } + } + err = r.Close() + if err != nil { + log.Println("ERROR: ", err) + } +} +``` + +Output: + +```output +Status: 200 OK +``` diff --git a/go.mod b/go.mod index f7d8718..b0bf8a2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module github.com/iguanesolutions/go-systemd/v5 go 1.13 + +require ( + github.com/godbus/dbus/v5 v5.0.4 + github.com/miekg/dns v1.1.42 + golang.org/x/net v0.0.0-20210525063256-abc453219eb5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c843d43 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY= +github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/resolved/dbus.go b/resolved/dbus.go new file mode 100644 index 0000000..687ac17 --- /dev/null +++ b/resolved/dbus.go @@ -0,0 +1,450 @@ +package resolved + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "strconv" + + "github.com/godbus/dbus/v5" + "github.com/miekg/dns" +) + +const ( + dbusDest = "org.freedesktop.resolve1" + dbusInterface = "org.freedesktop.resolve1.Manager" + dbusPath = "/org/freedesktop/resolve1" +) + +// Conn represents a systemd-resolved dbus connection. +type Conn struct { + conn *dbus.Conn + obj dbus.BusObject +} + +// NewConn returns a new and ready to use dbus connection. +// You must close that connection when you have been done with it. +func NewConn() (*Conn, error) { + conn, err := dbus.SystemBusPrivate() + if err != nil { + return nil, fmt.Errorf("failed to init private conn to system bus: %v", err) + } + methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} + err = conn.Auth(methods) + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to auth with external method: %v", err) + } + err = conn.Hello() + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to make hello call: %v", err) + } + return &Conn{ + conn: conn, + obj: conn.Object(dbusDest, dbus.ObjectPath(dbusPath)), + }, nil +} + +// Call wraps obj.CallWithContext by using 0 as flags and format the method with the dbus manager interface. +func (c *Conn) Call(ctx context.Context, method string, args ...interface{}) *dbus.Call { + return c.obj.CallWithContext(ctx, fmt.Sprintf("%s.%s", dbusInterface, method), 0, args...) +} + +// Close closes the current dbus connection. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// ResolveHostname, ResolveAddress, ResolveRecord, ResolveService +// The four methods above accept and return a 64-bit flags value. +// In most cases passing 0 is sufficient and recommended. +// However, the following flags are defined to alter the look-up +const ( + SD_RESOLVED_DNS = uint64(1) << 0 + SD_RESOLVED_LLMNR_IPV4 = uint64(1) << 1 + SD_RESOLVED_LLMNR_IPV6 = uint64(1) << 2 + SD_RESOLVED_MDNS_IPV4 = uint64(1) << 3 + SD_RESOLVED_MDNS_IPV6 = uint64(1) << 4 + SD_RESOLVED_NO_CNAME = uint64(1) << 5 + SD_RESOLVED_NO_TXT = uint64(1) << 6 + SD_RESOLVED_NO_ADDRESS = uint64(1) << 7 + SD_RESOLVED_NO_SEARCH = uint64(1) << 8 + SD_RESOLVED_AUTHENTICATED = uint64(1) << 9 +) + +// Address represents an address returned by ResolveHostname. +type Address struct { + IfIndex int // network interface index + Family int // can be either syscall.AF_INET or syscall.AF_INET6 + Address net.IP // binary address +} + +func (a Address) String() string { + return fmt.Sprintf(`{ + IfIndex: %d, + Family: %d, + IP: %s, +}`, a.IfIndex, a.Family, a.Address.String()) +} + +// ResolveHostname takes a hostname and resolves it to one or more IP addresses. +// ctx: Context to use +// ifindex: Network interface index where to look (0 means any) +// name: Hostname +// family: Which address family to look for (syscall.AF_UNSPEC, syscall.AF_INET, syscall.AF_INET6) +// flags: Input flags parameter +func (c *Conn) ResolveHostname(ctx context.Context, ifindex int, name string, family int, flags uint64) (addresses []Address, canonical string, outflags uint64, err error) { + err = c.Call(ctx, "ResolveHostname", ifindex, name, family, flags).Store(&addresses, &canonical, &outflags) + return +} + +// Name represents a hostname returned by ResolveAddress. +type Name struct { + IfIndex int // network interface index + Hostname string // hostname +} + +func (n Name) String() string { + return fmt.Sprintf(`{ + IfIndex: %d, + Name: %s, +}`, n.IfIndex, n.Hostname) +} + +// ResolveAddress takes a DNS resource record (RR) type, class and name +// and retrieves the full resource record set (RRset), including the RDATA, for it. +// ctx: Context to use +// ifindex: Network interface index where to look (0 means any) +// family: Address family (syscall.AF_INET, syscall.AF_INET6) +// address: the binary address (4 or 16 bytes) +// flags: Input flags parameter +func (c *Conn) ResolveAddress(ctx context.Context, ifindex int, family int, address net.IP, flags uint64) (names []Name, outflags uint64, err error) { + err = c.Call(ctx, "ResolveAddress", ifindex, family, address, flags).Store(&names, &outflags) + return +} + +// ResourceRecord represents a DNS RR as it returned by +// by ResolveRecord. +type ResourceRecord struct { + IfIndex int // network interface index + Type dns.Type // dns type + Class dns.Class // dns class + // The raw RR data starts with the RR's domain name, in the original casing, followed by the RR type, class, + // TTL and RDATA, in the binary format documented in RFC 1035. For RRs that support name compression in the payload + // (such as MX or PTR), the compression is expanded in the returned data. + Data []byte +} + +func (r ResourceRecord) String() string { + return fmt.Sprintf(`{ + IfIndex: %d, + Type: %s, + Class: %s, + Data: %v, +}`, r.IfIndex, r.Type.String(), r.Class.String(), r.Data) +} + +// Unpack unpacks a ResourceRecord to dns.RR interface. +func (r ResourceRecord) Unpack() (dns.RR, error) { + rr, _, err := dns.UnpackRR(r.Data, 0) + if err != nil { + return nil, err + } + return rr, nil +} + +// CNAME unpacks a ResourceRecord to *dns.CNAME. +func (r ResourceRecord) CNAME() (*dns.CNAME, error) { + rr, err := r.Unpack() + if err != nil { + return nil, err + } + if rr.Header().Rrtype != dns.TypeCNAME { + return nil, errors.New("not an CNAME record type") + } + cname, ok := rr.(*dns.CNAME) + if !ok { + return nil, errors.New("dns.RR is not a *dns.CNAME") + } + return cname, nil +} + +// MX unpacks a ResourceRecord to *dns.MX. +func (r ResourceRecord) MX() (*dns.MX, error) { + rr, err := r.Unpack() + if err != nil { + return nil, err + } + if rr.Header().Rrtype != dns.TypeMX { + return nil, errors.New("not an MX record type") + } + mx, ok := rr.(*dns.MX) + if !ok { + return nil, errors.New("dns.RR is not a *dns.MX") + } + return mx, nil +} + +// NS unpacks a ResourceRecord to *dns.NS. +func (r ResourceRecord) NS() (*dns.NS, error) { + rr, err := r.Unpack() + if err != nil { + return nil, err + } + if rr.Header().Rrtype != dns.TypeNS { + return nil, errors.New("not an NS record type") + } + ns, ok := rr.(*dns.NS) + if !ok { + return nil, errors.New("dns.RR is not a *dns.NS") + } + return ns, nil +} + +// SRV unpacks a ResourceRecord to *dns.SRV. +func (r ResourceRecord) SRV() (*dns.SRV, error) { + rr, err := r.Unpack() + if err != nil { + return nil, err + } + if rr.Header().Rrtype != dns.TypeSRV { + return nil, errors.New("not an SRV record type") + } + srv, ok := rr.(*dns.SRV) + if !ok { + return nil, errors.New("dns.RR is not a *dns.SRV") + } + return srv, nil +} + +// TXT unpacks a ResourceRecord to *dns.TXT. +func (r ResourceRecord) TXT() (*dns.TXT, error) { + rr, err := r.Unpack() + if err != nil { + return nil, err + } + if rr.Header().Rrtype != dns.TypeTXT { + return nil, errors.New("not an TXT record type") + } + txt, ok := rr.(*dns.TXT) + if !ok { + return nil, errors.New("dns.RR is not a *dns.TXT") + } + return txt, nil +} + +// ResolveRecord takes a DNS resource record (RR) type, class and name, and retrieves the full resource record set (RRset), including the RDATA, for it. +// ctx: Context to use +// ifindex: Network interface index where to look (0 means any) +// name: Specifies the RR domain name to look up +// class: 16-bit dns class +// rtype: 16-bit dns type +// flags: Input flags parameter +func (c *Conn) ResolveRecord(ctx context.Context, ifindex int, name string, class dns.Class, rtype dns.Type, flags uint64) (records []ResourceRecord, outflags uint64, err error) { + err = c.Call(ctx, "ResolveRecord", ifindex, name, class, rtype, flags).Store(&records, &outflags) + return +} + +// SRVRecord represents an service record as it returned +// by ResolveService. +type SRVRecord struct { + Priority uint16 + Weight uint16 + Port uint16 + Hostname string + Addresses []Address + CNAME string +} + +func (r SRVRecord) String() string { + return fmt.Sprintf(`{ + Priority: %d, + Weight: %d, + Port: %d, + Hostname: %s, + Addresses: %v, +}`, r.Priority, r.Weight, r.Port, r.Hostname, r.Addresses) +} + +// TXTRecord represents a raw TXT RR string +type TXTRecord []byte + +func (r TXTRecord) String() string { + return string(r) +} + +// ResolveService resolves a DNS SRV service record, as well as the hostnames referenced in it +// and possibly an accompanying DNS-SD TXT record containing additional service metadata. +// ctx: Context to use +// ifindex: Network interface index where to look (0 means any) +// name: the service name +// stype: the service type (eg: _webdav._tcp) +// domain: the service domain +// family: Address family (syscall.AF_UNSPEC, syscall.AF_INET, syscall.AF_INET6) +// flags: Input flags parameter +func (c *Conn) ResolveService(ctx context.Context, ifindex int, name string, stype string, domain string, family int, + flags uint64) (srvData []SRVRecord, txtData []TXTRecord, canonicalName string, canonicalType string, canonicalDomain string, outflags uint64, err error) { + err = c.Call(ctx, "ResolveService", ifindex, name, stype, domain, family, flags).Store(&srvData, &txtData, &canonicalName, &canonicalType, &canonicalDomain, &outflags) + return +} + +// GetLink takes a network interface index and returns the object path +// to the org.freedesktop.resolve1.Link object corresponding to it. +// ctx: Context to use +// ifindex: The network interface index to get link for +func (c *Conn) GetLink(ctx context.Context, ifindex int) (path string, err error) { + err = c.Call(ctx, "GetLink", ifindex).Store(&path) + return +} + +// LinkDNS represents a DNS server address to use in SetLinkDNS method. +type LinkDNS struct { + Family int // can be either syscall.AF_INET or syscall.AF_INET6 + Address net.IP // binary address +} + +// SetLinkDNS sets the DNS servers to use on a specific interface. +// ctx: Context to use +// ifindex: The network interface index to set +// addrs: array of DNS server IP address records. +func (c *Conn) SetLinkDNS(ctx context.Context, ifindex int, addrs []LinkDNS) (err error) { + err = c.Call(ctx, "SetLinkDNS", ifindex, addrs).Store() + return +} + +type LinkDNSEx struct { + Family int // can be either syscall.AF_INET or syscall.AF_INET6 + Address net.IP // binary address + Port uint16 // the port number + Name string // the DNS Name +} + +// SetLinkDNSEx is similar to SetLinkDNS(), but allows an IP port +// (instead of the default 53) and DNS name to be specified for each DNS server. +// The server name is used for Server Name Indication (SNI), which is useful when DNS-over-TLS is used. +// ctx: Context to use +// ifindex: The network interface index +// addrs: array of DNS server IP address records. +func (c *Conn) SetLinkDNSEx(ctx context.Context, ifindex int, addrs []LinkDNSEx) error { + return c.Call(ctx, "SetLinkDNSEx", ifindex, addrs).Store() +} + +type LinkDomain struct { + Domain string // the domain name + RoutingDomain bool // whether the specified domain shall be used as a search domain (false), or just as a routing domain (true). +} + +// SetLinkDomains sets the search and routing domains to use on a specific network interface for DNS look-ups. +// ctx: Context to use +// ifindex: The network interface index +// domains: array of domains +func (c *Conn) SetLinkDomains(ctx context.Context, ifindex int, domains []LinkDomain) error { + return c.Call(ctx, "SetLinkDomains", ifindex, domains).Store() +} + +// SetLinkDefaultRoute specifies whether the link shall be used as the default route for name queries +// ctx: Context to use +// ifindex: The network interface index +// enable: enable/disable link as default route. +func (c *Conn) SetLinkDefaultRoute(ctx context.Context, ifindex int, enable bool) error { + return c.Call(ctx, "SetLinkDefaultRoute", ifindex, enable).Store() +} + +// SetLinkLLMNR enables or disables LLMNR support on a specific network interface. +// ctx: Context to use +// ifindex: The network interface index +// mode: either empty or one of "yes", "no" or "resolve". +func (c *Conn) SetLinkLLMNR(ctx context.Context, ifindex int, mode string) error { + return c.Call(ctx, "SetLinkLLMNR", ifindex, mode).Store() +} + +// SetLinkMulticastDNS enables or disables MulticastDNS support on a specific interface. +// ctx: Context to use +// ifindex: The network interface index +// mode: either empty or one of "yes", "no" or "resolve". +func (c *Conn) SetLinkMulticastDNS(ctx context.Context, ifindex int, mode string) error { + return c.Call(ctx, "SetLinkMulticastDNS", ifindex, mode).Store() +} + +// SetLinkDNSOverTLS enables or disables enables or disables DNS-over-TLS on a specific network interface. +// ctx: Context to use +// ifindex: The network interface index +// mode: either empty or one of "yes", "no", or "opportunistic" +func (c *Conn) SetLinkDNSOverTLS(ctx context.Context, ifindex int, mode string) error { + return c.Call(ctx, "SetLinkDNSOverTLS", ifindex, mode).Store() +} + +// SetLinkDNSSEC enables or disables DNSSEC validation on a specific network interface. +// ctx: Context to use +// ifindex: The network interface index +// mode: either empty or one of "yes", "no", or "allow-downgrade" +func (c *Conn) SetLinkDNSSEC(ctx context.Context, ifindex int, mode string) error { + return c.Call(ctx, "SetLinkDNSSEC", ifindex, mode).Store() +} + +// SetLinkDNSSECNegativeTrustAnchors configures DNSSEC Negative Trust Anchors (NTAs) for a specific network interface. +// ctx: Context to use +// ifindex: The network interface index +// names: array of domains +func (c *Conn) SetLinkDNSSECNegativeTrustAnchors(ctx context.Context, ifindex int, names []string) error { + return c.Call(ctx, "SetLinkDNSSECNegativeTrustAnchors", ifindex, names).Store() +} + +// RevertLink reverts all per-link settings to the defaults on a specific network interface. +// ctx: Context to use +// ifindex: The network interface index. +func (c *Conn) RevertLink(ctx context.Context, ifindex int) error { + return c.Call(ctx, "RevertLink", ifindex).Store() +} + +// RegisterService +func (c *Conn) RegisterService(ctx context.Context, name string, nameTemplate string, stype string, + svcPort uint16, svcPriority uint16, svcWeight uint16, txtData []TXTRecord) (svcPath string, err error) { + err = c.Call(ctx, "RegisterService", name, nameTemplate, stype, svcPort, svcPriority, svcWeight, txtData).Store(&svcPath) + return +} + +// UnregisterService +func (c *Conn) UnregisterService(ctx context.Context, svcPath string) error { + return c.Call(ctx, "UnregisterService", svcPath).Store() +} + +// ResetStatistics resets the various statistics counters that systemd-resolved maintains to zero. +func (c *Conn) ResetStatistics(ctx context.Context) error { + return c.Call(ctx, "ResetStatistics").Store() +} + +// FlushCaches +func (c *Conn) FlushCaches(ctx context.Context) error { + return c.Call(ctx, "FlushCaches").Store() +} + +// ResetServerFeatures +func (c *Conn) ResetServerFeatures(ctx context.Context) error { + return c.Call(ctx, "ResetServerFeatures").Store() +} + +type Link struct { + obj dbus.BusObject +} + +func NewLink(c *Conn, path string) Link { + return Link{ + obj: c.conn.Object(dbusDest, dbus.ObjectPath(path)), + } +} + +// TODO +// SetDNS(in a(iay) addresses); +// SetDNSEx(in a(iayqs) addresses); +// SetDomains(in a(sb) domains); +// SetDefaultRoute(in b enable); +// SetLLMNR(in s mode); +// SetMulticastDNS(in s mode); +// SetDNSOverTLS(in s mode); +// SetDNSSEC(in s mode); +// SetDNSSECNegativeTrustAnchors(in as names); +// Revert(); diff --git a/resolved/resolver.go b/resolved/resolver.go new file mode 100644 index 0000000..b5f4a26 --- /dev/null +++ b/resolved/resolver.go @@ -0,0 +1,472 @@ +package resolved + +import ( + "context" + "errors" + "net" + "net/http" + "runtime" + "sort" + "syscall" + "time" + + "github.com/miekg/dns" + "golang.org/x/net/idna" +) + +// Note: This is still under development and very experimental, do not use it in production. + +// resolver is the interface to implements the same methods as the net.Resolver +type resolver interface { + LookupAddr(ctx context.Context, addr string) (names []string, err error) + LookupCNAME(ctx context.Context, host string) (cname string, err error) + LookupHost(ctx context.Context, host string) (addrs []string, err error) + LookupIP(ctx context.Context, network, host string) ([]net.IP, error) + LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) + LookupMX(ctx context.Context, name string) ([]*net.MX, error) + LookupNS(ctx context.Context, name string) ([]*net.NS, error) + LookupPort(ctx context.Context, network, service string) (port int, err error) + LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error) + LookupTXT(ctx context.Context, name string) ([]string, error) +} + +var ( + // ensure that types implement resolver interface + _ resolver = &Resolver{} + _ resolver = &net.Resolver{} +) + +// Resolver represents the systemd-resolved resolver +// throught dbus connection. +type Resolver struct { + conn *Conn + dialer *net.Dialer + profile *idna.Profile +} + +type resolverOption func(r *Resolver) error + +// WithConn allow you to use a custom systemd-resolved dbus connection. +func WithConn(c *Conn) resolverOption { + return func(r *Resolver) error { + if c == nil { + return errors.New("conn is nil") + } + r.conn = c + return nil + } +} + +// WithDialer allow you to use a custom net.Dialer. +func WithDialer(d *net.Dialer) resolverOption { + return func(r *Resolver) error { + if d == nil { + return errors.New("dialer is nil") + } + r.dialer = d + return nil + } +} + +// WithProfile allow you to use custom idna.Profile. +func WithProfile(p *idna.Profile) resolverOption { + return func(r *Resolver) error { + if p == nil { + return errors.New("profile is nil") + } + r.profile = p + return nil + } +} + +// NewResolver returns a new systemd Resolver with an initialized dbus connection. +// it's up to you to close that connection when you have been done with the Resolver. +func NewResolver(opts ...resolverOption) (*Resolver, error) { + r := &Resolver{} + for _, opt := range opts { + opt(r) + } + if r.conn == nil { + var err error + r.conn, err = NewConn() + if err != nil { + return nil, err + } + } + if r.dialer == nil { + r.dialer = &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + } + if r.profile == nil { + r.profile = idna.New() + } + return r, nil +} + +// Close closes the current dbus connection. +// You need to close the connection when you've done with it. +func (r *Resolver) Close() error { + return r.conn.Close() +} + +// DialContext resolves address using systemd-network and use internal dialer with the resolved ip address. +// It is useful when it comes to integration with go standard library. +func (r *Resolver) DialContext(ctx context.Context, network string, address string) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + addrs, _, _, err := r.conn.ResolveHostname(ctx, 0, host, syscall.AF_UNSPEC, 0) + if err != nil { + return nil, err + } + for _, addr := range addrs { + if addr.Address.To4() == nil { + // prefer ipv6 + address = addr.Address.String() + break + } + address = addr.Address.String() + } + return r.dialer.DialContext(ctx, network, net.JoinHostPort(address, port)) +} + +// HTTPClient returns a new http.Client with systemd-resolved as resolver +// and idle connections + keepalives disabled. +func (r *Resolver) HTTPClient() *http.Client { + transport := r.pooledTransport() + transport.DisableKeepAlives = true + transport.MaxIdleConnsPerHost = -1 + return &http.Client{ + Transport: transport, + } +} + +// HTTPPooledClient returns a new http.Client with systemd-resolved as resolver +// and similar default values to http.DefaultTransport. +// Do not use this for transient transports as +// it can leak file descriptors over time. Only use this for transports that +// will be re-used for the same host(s). +func (r *Resolver) HTTPPooledClient() *http.Client { + return &http.Client{ + Transport: r.pooledTransport(), + } +} + +func (r *Resolver) pooledTransport() *http.Transport { + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: r.DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + ForceAttemptHTTP2: true, + MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, + } + return transport +} + +// LookupHost looks up the given host using the systemd-resolved resolver. +// It returns a slice of that host's addresses. +func (r *Resolver) LookupHost(ctx context.Context, host string) (addrs []string, err error) { + if host == "" { + return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} + } + addresses, _, _, err := r.conn.ResolveHostname(ctx, 0, host, syscall.AF_UNSPEC, 0) + if err != nil { + return nil, err + } + addrs = make([]string, len(addresses)) + for i, addr := range addresses { + addrs[i] = addr.Address.String() + } + return +} + +// LookupAddr performs a reverse lookup for the given address, returning a list +// of names mapping to that address. +func (r *Resolver) LookupAddr(ctx context.Context, addr string) (names []string, err error) { + ip := net.ParseIP(addr) + if ip == nil { + return nil, &net.DNSError{Err: "unrecognized address", Name: addr} + } + var family int + if ipv4 := ip.To4(); ipv4 != nil { + // use 4-byte representation + ip = ipv4 + family = syscall.AF_INET + } else { + family = syscall.AF_INET6 + } + hostnames, _, err := r.conn.ResolveAddress(ctx, 0, family, ip, 0) + if err != nil { + return nil, err + } + names = make([]string, len(hostnames)) + for i, name := range hostnames { + names[i] = fullyQualified(name.Hostname) + } + return +} + +// LookupIP looks up host for the given network using the systemd-resolved resolver. +// It returns a slice of that host's IP addresses of the type specified by network. +// network must be one of "ip", "ip4" or "ip6". +func (r *Resolver) LookupIP(ctx context.Context, network, host string) ([]net.IP, error) { + if host == "" { + return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} + } + var family int + switch network { + case "ip": + family = syscall.AF_UNSPEC + case "ip4": + family = syscall.AF_INET + case "ip6": + family = syscall.AF_INET6 + default: + return nil, errors.New("bad network") + } + addresses, _, _, err := r.conn.ResolveHostname(ctx, 0, host, family, 0) + if err != nil { + return nil, err + } + addrs := make([]net.IP, len(addresses)) + for i, addr := range addresses { + addrs[i] = addr.Address + } + return addrs, nil +} + +// LookupIPAddr looks up host using the systemd-resolved resolver. +// It returns a slice of that host's IPv4 and IPv6 addresses. +func (r *Resolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) { + if host == "" { + return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} + } + addresses, _, _, err := r.conn.ResolveHostname(ctx, 0, host, syscall.AF_UNSPEC, 0) + if err != nil { + return nil, err + } + addrs := make([]net.IPAddr, len(addresses)) + for i, addr := range addresses { + addrs[i] = net.IPAddr{ + IP: addr.Address, + } + } + return addrs, nil +} + +// LookupCNAME returns the canonical name for the given host. +func (r *Resolver) LookupCNAME(ctx context.Context, host string) (string, error) { + var ok bool + if host, ok = r.IsDomainName(host); !ok { + return "", &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} + } + records, _, err := r.conn.ResolveRecord(ctx, 0, host, dns.ClassINET, dns.Type(dns.TypeCNAME), 0) + if err != nil { + return "", err + } + for _, record := range records { + recordCNAME, err := record.CNAME() + if err != nil { + return "", err + } + return recordCNAME.Target, nil + } + return "", &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} +} + +// LookupMX returns the DNS MX records for the given domain name sorted by preference. +func (r *Resolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) { + var ok bool + if name, ok = r.IsDomainName(name); !ok { + return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true} + } + records, _, err := r.conn.ResolveRecord(ctx, 0, name, dns.ClassINET, dns.Type(dns.TypeMX), 0) + if err != nil { + return nil, err + } + mxs := make([]*net.MX, len(records)) + for i, record := range records { + mx, err := record.MX() + if err != nil { + return nil, err + } + mxs[i] = &net.MX{ + Host: mx.Mx, + Pref: mx.Preference, + } + } + sort.Slice(mxs, func(i, j int) bool { + return mxs[i].Pref < mxs[j].Pref + }) + return mxs, nil +} + +// LookupNS returns the DNS NS records for the given domain name. +func (r *Resolver) LookupNS(ctx context.Context, name string) ([]*net.NS, error) { + var ok bool + if name, ok = r.IsDomainName(name); !ok { + return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true} + } + records, _, err := r.conn.ResolveRecord(ctx, 0, name, dns.ClassINET, dns.Type(dns.TypeNS), 0) + if err != nil { + return nil, err + } + nss := make([]*net.NS, len(records)) + for i, record := range records { + ns, err := record.NS() + if err != nil { + return nil, err + } + nss[i] = &net.NS{ + Host: ns.Ns, + } + } + return nss, nil +} + +// LookupPort looks up the port for the given network and service. +func (r *Resolver) LookupPort(ctx context.Context, network, service string) (port int, err error) { + // this is not supported because i don't want to implement again what's inside the go standard library + // like the port map filled with /etc/service etc... + err = errors.New("not supported yet") + return +} + +// LookupSRV tries to resolve an SRV query of the given service, protocol, and domain name. +// The proto is "tcp" or "udp". The returned records are sorted by priority. +func (r *Resolver) LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error) { + var target string + if service == "" && proto == "" { + target = name + } else { + target = "_" + service + "._" + proto + "." + name + } + srvData, _, _, canonicalType, canonicalDomain, _, err := r.conn.ResolveService(ctx, 0, "", "", target, syscall.AF_UNSPEC, 0) + if err != nil { + return + } + addrs = make([]*net.SRV, len(srvData)) + for i, srv := range srvData { + addrs[i] = &net.SRV{ + Target: fullyQualified(srv.Hostname), + Port: srv.Port, + Priority: srv.Priority, + Weight: srv.Weight, + } + } + sort.Slice(addrs, func(i, j int) bool { + return addrs[i].Priority < addrs[j].Priority + }) + if canonicalType != "" { + cname = fullyQualified(canonicalType + "." + canonicalDomain) + } else { + cname = fullyQualified(canonicalDomain) + } + return +} + +// LookupTXT returns the DNS TXT records for the given domain name. +func (r *Resolver) LookupTXT(ctx context.Context, name string) ([]string, error) { + var ok bool + if name, ok = r.IsDomainName(name); !ok { + return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true} + } + records, _, err := r.conn.ResolveRecord(ctx, 0, name, dns.ClassINET, dns.Type(dns.TypeTXT), 0) + if err != nil { + return nil, err + } + txts := make([]string, 0, len(records)) + for _, record := range records { + txt, err := record.TXT() + if err != nil { + return nil, err + } + txts = append(txts, txt.Txt...) + } + return txts, nil +} + +// IsDomainName tries to convert name to ASCII (IANA conversion) if name is not a strict domain name (see RFC 1035) +// It returns false if name is not a domain before and after ASCII conversion. +// It uses isDomainName from go standard library. +func (r *Resolver) IsDomainName(name string) (string, bool) { + if !isDomainName(name) { + var err error + name, err = r.profile.ToASCII(name) + if err != nil { + return name, false + } + if !isDomainName(name) { + return name, false + } + } + return name, true +} + +func fullyQualified(s string) string { + b := []byte(s) + hasDots := false + for _, x := range b { + if x == '.' { + hasDots = true + break + } + } + if hasDots && b[len(b)-1] != '.' { + b = append(b, '.') + } + return string(b) +} + +// this function comes from go standard library +// there is issues about it since it denied some valid domains. +// see: https://github.com/golang/go/issues/17659 +func isDomainName(s string) bool { + l := len(s) + if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { + return false + } + last := byte('.') + nonNumeric := false // true once we've seen a letter or hyphen + partlen := 0 + for i := 0; i < len(s); i++ { + c := s[i] + switch { + default: + return false + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': + nonNumeric = true + partlen++ + case '0' <= c && c <= '9': + // fine + partlen++ + case c == '-': + // Byte before dash cannot be dot. + if last == '.' { + return false + } + partlen++ + nonNumeric = true + case c == '.': + // Byte before dot cannot be dot, dash. + if last == '.' || last == '-' { + return false + } + if partlen > 63 || partlen == 0 { + return false + } + partlen = 0 + } + last = c + } + if last == '-' || partlen > 63 { + return false + } + return nonNumeric +} diff --git a/resolved/resolver_test.go b/resolved/resolver_test.go new file mode 100644 index 0000000..551c48d --- /dev/null +++ b/resolved/resolver_test.go @@ -0,0 +1,488 @@ +package resolved + +import ( + "context" + "net" + "net/http" + "sort" + "testing" +) + +// In order to run the test make sure that systemd-resolved resolver query the same dns server as the go one. + +const ( + lookupHost = "google.com" + lookupAddr4 = "142.250.178.142" + lookupAddr6 = "2a00:1450:4007:81a::200e" + lookupCNAMEHost = "en.wikipedia.org" + lookupSRVDomain = "google.com" + lookupSRVService = "xmpp-server" + lookupSRVProto = "tcp" + lookupSRVServiceDomain = "_xmpp-server._tcp.google.com" + getUrl = "https://google.com" +) + +func TestLookupHost(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdAddrs, err := sysdResolver.LookupHost(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goAddrs, err := goResolver.LookupHost(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + if len(goAddrs) != len(sysdAddrs) { + t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) + } + sort.Strings(sysdAddrs) + sort.Strings(goAddrs) + for i, sAddr := range sysdAddrs { + goAddr := goAddrs[i] + if goAddr != sAddr { + t.Error("goAddr != sAddr", goAddr, sAddr) + } + } +} + +func TestLookupAddr4(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdNames, err := sysdResolver.LookupAddr(ctx, lookupAddr4) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goNames, err := goResolver.LookupAddr(ctx, lookupAddr4) + if err != nil { + t.Fatal(err) + } + if len(goNames) != len(sysdNames) { + t.Fatal("len(goNames) != len(sysdNames)", len(goNames), len(sysdNames)) + } + sort.Strings(goNames) + sort.Strings(sysdNames) + for i, sName := range sysdNames { + goName := goNames[i] + if goName != sName { + t.Error("goName != sName", goName, sName) + } + } +} + +func TestLookupAddr6(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdNames, err := sysdResolver.LookupAddr(ctx, lookupAddr6) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goNames, err := goResolver.LookupAddr(ctx, lookupAddr6) + if err != nil { + t.Fatal(err) + } + if len(goNames) != len(sysdNames) { + t.Fatal("len(goNames) != len(sysdNames)", len(goNames), len(sysdNames)) + } + sort.Strings(goNames) + sort.Strings(sysdNames) + for i, sName := range sysdNames { + goName := goNames[i] + if goName != sName { + t.Error("goName != sName", goName, sName) + } + } +} + +func TestLookupIP(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdAddrs, err := sysdResolver.LookupIP(ctx, "ip", lookupHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goAddrs, err := goResolver.LookupIP(ctx, "ip", lookupHost) + if err != nil { + t.Fatal(err) + } + if len(goAddrs) != len(sysdAddrs) { + t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) + } + sort.Slice(sysdAddrs, func(i, j int) bool { + return sysdAddrs[i].String() < sysdAddrs[j].String() + }) + sort.Slice(goAddrs, func(i, j int) bool { + return goAddrs[i].String() < goAddrs[j].String() + }) + for i, sAddr := range sysdAddrs { + goAddr := goAddrs[i] + if goAddr.String() != sAddr.String() { + t.Error("goAddr != sAddr", goAddr, sAddr) + } + } +} + +func TestLookupIP4(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdAddrs, err := sysdResolver.LookupIP(ctx, "ip4", lookupHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goAddrs, err := goResolver.LookupIP(ctx, "ip4", lookupHost) + if err != nil { + t.Fatal(err) + } + if len(goAddrs) != len(sysdAddrs) { + t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) + } + sort.Slice(sysdAddrs, func(i, j int) bool { + return sysdAddrs[i].String() < sysdAddrs[j].String() + }) + sort.Slice(goAddrs, func(i, j int) bool { + return goAddrs[i].String() < goAddrs[j].String() + }) + for i, sAddr := range sysdAddrs { + goAddr := goAddrs[i] + if goAddr.String() != sAddr.String() { + t.Error("goAddr != sAddr", goAddr, sAddr) + } + } +} + +func TestLookupIP6(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdAddrs, err := sysdResolver.LookupIP(ctx, "ip6", lookupHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goAddrs, err := goResolver.LookupIP(ctx, "ip6", lookupHost) + if err != nil { + t.Fatal(err) + } + if len(goAddrs) != len(sysdAddrs) { + t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) + } + sort.Slice(sysdAddrs, func(i, j int) bool { + return sysdAddrs[i].String() < sysdAddrs[j].String() + }) + sort.Slice(goAddrs, func(i, j int) bool { + return goAddrs[i].String() < goAddrs[j].String() + }) + for i, sAddr := range sysdAddrs { + goAddr := goAddrs[i] + if goAddr.String() != sAddr.String() { + t.Error("goAddr != sAddr", goAddr, sAddr) + } + } +} + +func TestLookupIPAddr(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdAddrs, err := sysdResolver.LookupIPAddr(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goAddrs, err := goResolver.LookupIPAddr(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + if len(goAddrs) != len(sysdAddrs) { + t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) + } + sort.Slice(sysdAddrs, func(i, j int) bool { + return sysdAddrs[i].String() < sysdAddrs[j].String() + }) + sort.Slice(goAddrs, func(i, j int) bool { + return goAddrs[i].String() < goAddrs[j].String() + }) + for i, sAddr := range sysdAddrs { + goAddr := goAddrs[i] + if goAddr.String() != sAddr.String() { + t.Error("goAddr != sAddr", goAddr, sAddr) + } + if goAddr.Zone != sAddr.Zone { + t.Error("goAddr .Zone!= sAddr.Zone", goAddr.Zone, sAddr.Zone) + } + } +} + +func TestLookupCNAME(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdCNAME, err := sysdResolver.LookupCNAME(ctx, lookupCNAMEHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goCNAME, err := goResolver.LookupCNAME(ctx, lookupCNAMEHost) + if err != nil { + t.Fatal(err) + } + if goCNAME != sysdCNAME { + t.Error("goCNAME != sysdCNAME", goCNAME, sysdCNAME) + } +} + +func TestLookupMX(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdMxs, err := sysdResolver.LookupMX(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goMxs, err := goResolver.LookupMX(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + if len(goMxs) != len(sysdMxs) { + t.Fatal("len(goMxs) != len(sysdMxs)", len(goMxs), len(sysdMxs)) + } + for i, sMx := range sysdMxs { + goMx := goMxs[i] + if goMx.Host != sMx.Host { + t.Error("goMx.Host != sMx.Host", goMx.Host, sMx.Host) + } + if goMx.Pref != sMx.Pref { + t.Error("goMx.Pref != sMx.Pref", goMx.Pref, sMx.Pref) + } + } +} + +func TestLookupNS(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdNss, err := sysdResolver.LookupNS(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goNss, err := goResolver.LookupNS(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + if len(goNss) != len(sysdNss) { + t.Fatal("len(goNss) != len(sysdNss)", len(goNss), len(sysdNss)) + } + sort.Slice(sysdNss, func(i, j int) bool { + return sysdNss[i].Host < sysdNss[j].Host + }) + sort.Slice(goNss, func(i, j int) bool { + return goNss[i].Host < goNss[j].Host + }) + for i, sNs := range sysdNss { + goNs := goNss[i] + if goNs.Host != sNs.Host { + t.Error("goNs.Host != sNs.Host", goNs.Host, sNs.Host) + } + } +} + +func TestLookupSRV(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdCNAME, sysdSrvs, err := sysdResolver.LookupSRV(ctx, lookupSRVService, lookupSRVProto, lookupSRVDomain) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goCNAME, goSrvs, err := goResolver.LookupSRV(ctx, lookupSRVService, lookupSRVProto, lookupSRVDomain) + if err != nil { + t.Fatal(err) + } + if sysdCNAME != goCNAME { + t.Fatal("sysdCNAME != goCNAME", sysdCNAME, goCNAME) + } + if len(goSrvs) != len(sysdSrvs) { + t.Fatal("len(goSrvs) != len(sysdSrvs)", len(goSrvs), len(sysdSrvs)) + } + sort.Slice(sysdSrvs, func(i, j int) bool { + return sysdSrvs[i].Target < sysdSrvs[j].Target + }) + sort.Slice(goSrvs, func(i, j int) bool { + return goSrvs[i].Target < goSrvs[j].Target + }) + for i, sSrv := range sysdSrvs { + goSrv := goSrvs[i] + if goSrv.Target != sSrv.Target { + t.Error("goSrv.Target != sSrv.Target", goSrv.Target, sSrv.Target) + } + if goSrv.Port != sSrv.Port { + t.Error("goSrv.Port != sSrv.Port", goSrv.Port, sSrv.Port) + } + if goSrv.Priority != sSrv.Priority { + t.Error("goSrv.Priority != sSrv.Priority", goSrv.Priority, sSrv.Priority) + } + if goSrv.Weight != sSrv.Weight { + t.Error("goSrv.Weight != sSrv.Weight", goSrv.Weight, sSrv.Weight) + } + } +} + +func TestLookupTXT(t *testing.T) { + sysdResolver, err := NewResolver() + if err != nil { + t.Fatal(err) + } + defer sysdResolver.Close() + ctx := context.Background() + sysdTxts, err := sysdResolver.LookupTXT(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + goResolver := &net.Resolver{} + goTxts, err := goResolver.LookupTXT(ctx, lookupHost) + if err != nil { + t.Fatal(err) + } + if len(goTxts) != len(sysdTxts) { + t.Fatal("len(goTxts) != len(sysdTxts)", len(goTxts), len(sysdTxts)) + } + sort.Strings(sysdTxts) + sort.Strings(goTxts) + for i, sTxt := range sysdTxts { + goTxt := goTxts[i] + if goTxt != sTxt { + t.Error("goTxt != sTxt", goTxt, sTxt) + } + } +} + +func BenchmarkLookupHostGoResolver(b *testing.B) { + r := &net.Resolver{} + ctx := context.Background() + for n := 0; n < b.N; n++ { + _, err := r.LookupHost(ctx, lookupHost) + if err != nil { + b.Error(err) + continue + } + } +} + +func BenchmarkLookupHostSystemdResolver(b *testing.B) { + r, err := NewResolver() + if err != nil { + b.Fatal(err) + } + defer r.Close() + ctx := context.Background() + for n := 0; n < b.N; n++ { + _, err := r.LookupHost(ctx, lookupHost) + if err != nil { + b.Error(err) + continue + } + } +} + +func BenchmarkLookupAddrGoResolver(b *testing.B) { + r := &net.Resolver{} + ctx := context.Background() + for n := 0; n < b.N; n++ { + _, err := r.LookupAddr(ctx, lookupAddr4) + if err != nil { + b.Error(err) + continue + } + } +} + +func BenchmarkLookupAddrSystemdResolver(b *testing.B) { + r, err := NewResolver() + if err != nil { + b.Fatal(err) + } + defer r.Close() + ctx := context.Background() + for n := 0; n < b.N; n++ { + _, err := r.LookupAddr(ctx, lookupAddr4) + if err != nil { + b.Error(err) + continue + } + } +} + +func BenchmarkHTTPClientGoResolver(b *testing.B) { + httpCli := &http.Client{} + for n := 0; n < b.N; n++ { + resp, err := httpCli.Get(getUrl) + if err != nil { + b.Error(err) + continue + } + resp.Body.Close() + } +} + +func BenchmarkHTTPClientSystemdResolver(b *testing.B) { + r, err := NewResolver() + if err != nil { + b.Fatal(err) + } + defer r.Close() + httpCli := r.HTTPClient() + for n := 0; n < b.N; n++ { + resp, err := httpCli.Get(getUrl) + if err != nil { + b.Error(err) + continue + } + resp.Body.Close() + } +}